Skip to main content

Command Palette

Search for a command to run...

There Is No Server to Audit — GenosDB's First Security Audit, and What It Found

What an audit means when authorization lives on every device, how a closed-source engine can still be checked line by line, and why the best result of this run was a finding that did not survive.

Updated
11 min readView as Markdown
There Is No Server to Audit — GenosDB's First Security Audit, and What It Found
E

Full Stack Developer - dWEB R&D

When you audit a normal database application, the first thing you do is ask where the server is. That is where authorization lives: one process, one place that decides who may read and who may write, one boundary to push against. Find a way past it and you have a finding.

GenosDB has no server. Every peer holds the whole graph, every peer may run modified code, and the relays that carry the traffic are assumed hostile. There is no single place where permission is granted, because permission is not granted anywhere — it is verified, independently, by every device that receives an operation.

So what do you audit? That question is more interesting than it sounds, and answering it is what the latest release came out of.

What a Finding Even Means Without a Server

In a client-server system, the threat model is implicit: the server is honest, the client is not. In GenosDB it has to be stated, because nothing is implicit. It is written down in SECURITY.md, and it is blunt:

Any peer may run modified code, and the network — relays, superpeers, other peers — is hostile. A peer decides on its own copy only; every other peer verifies what it receives and applies nothing it cannot verify.

Read that as an auditor and the target becomes clear. You are not looking for a way past a gate. You are looking for one thing:

Can a peer make another peer apply, accept or reveal something outside its authority?

That is the whole question. Not "can I send a bad operation" — of course you can, you control your own client. The question is whether the receiver applies it. A modified peer that writes nonsense into its own copy has achieved nothing; it has lied to itself. A modified peer that makes an honest peer store a role it never granted, or open a record it was never given the key to, has found something real.

What a receiver decides: the signature and the author it recovers, the author's role as this peer holds it, and the node's policy on this peer's own copy — three checks, each read from the receiver's copy and each a refusal on its own; only then is the operation applied and the clock moved.

This also means a whole class of "vulnerabilities" evaporates on contact. A room opened without the Security Manager has no gate, by design — anyone may write, and that is the documented behaviour of a public room, not a defect. Discipline about that distinction is most of what separates a useful audit from a long list of things that were never promised.

Closed Source, Open Protocol — and Why That Makes It Auditable

The engine ships minified. That usually ends the conversation with an auditor, and for a long time it ended it here too.

It does not any more, because what the engine does is fully specified. CRYPTOGRAPHY.md describes identity derivation, what exactly is signed and in what canonical form, how signatures are verified and authorization decided, ordering and replay, the encrypted-record envelopes, passkeys, signaling and transport, and what is written to disk. Its last section is a list of commands anyone can run against the published bundle to confirm the code matches the document.

And there is a second layer that matters more than the first: the conformance vectors. They are 98 checks re-derived from the specification alone — WebCrypto for HKDF and AES-GCM, @noble/curves for secp256k1, @noble/hashes for keccak-256, msgpack and zlib. No GenosDB code runs in the verifier. If the spec and the bundle ever disagree, the vectors say so without anyone having to take my word for it:

npm pack genosdb@latest && tar xzf genosdb-*.tgz
node tests/vectors/verify.mjs --dist package/dist

A meta.json pins the SHA-256 of the exact dist/ files the vectors were produced beside, so the bundle you hold can be bound to the numbers you are checking. That is the difference between "trust the source" and "check the artifact".

How the Run Was Organized

The audit was run as a structured campaign under Cloudflare's security-audit skill, with a discipline I would keep regardless of the tooling:

  • Coverage first. Twenty-two units, each one a triple of surface × trust boundary × attack class, so the run could say what it had looked at and, just as importantly, what it had not.
  • Hunting and verification are different jobs. Every candidate was handed to a verifier that had not found it, and no verdict was accepted without a working reproduction — not an argument, a harness that runs.
  • Every refusal was re-read. A candidate that was dismissed went to a second verifier who was not shown the first conclusion. If a rejection was a misreading, this is what catches it.
  • Black box in parallel. A separate matrix of 46 cases, built with a different toolchain and no access to the sources, attacked the published bundles from a hostile browser over real WebRTC — a second graph, a second origin, its own storage.

The thing I would emphasise to anyone auditing a P2P system: run the negative controls. "The data appeared on the other peer" proves nothing on its own — it may have arrived through shared storage rather than the network, and a refusal may be a disconnection rather than a decision. Every negative case in that matrix carried a legitimate witness operation that had to arrive by the same path, so a silent failure could never be mistaken for a rejection.

What Held

Forty of the 46 black-box cases held, against an attacker that controlled its own packets and its own identity: unsigned operations on all four sync paths; tampering with content, id, identity, address, timestamp and signature; guest writes, deletes and links; self-escalation to admin and superadmin; role claims planted in a first profile; demotion; replay of old promotions; ACL nodes without a grant and with a forged owner; a clock run forward; a bounded corpus of invalid signatures; a revoked reader trying to read what came after.

Twelve of the 22 coverage units closed with nothing to report — both authorship gates against a modified peer, catch-up and ordering, persistence and cross-tab copies, envelope cryptography and passkey custody, transport framing and the cellular overlay, signaling and the embedded relay, the release chain.

The Best Result Was a Finding That Did Not Survive

My favourite moment of the whole run was a candidate that turned out to be wrong.

It looked clean. assignRole(address, role, expiresAt) grants a role until a date. Let the date pass, and a standing governance rule promotes that identity straight back to a permanent role. Written as a bug report, it reads like a serious one: an expiry that does not stick.

Except an expiry in GenosDB is not a countdown some engine owns. It is a term inside a signed decision, and every peer enforces it locally: past the date, that identity is a guest on every device, with nobody connected and no engine running anywhere. And the project's own decision log says what an expiry promises — it is final until the constitution decides again. A governance rule is the constitution deciding: the engine signs with a superadmin's key or it does not sign at all.

The verifier proved the other side of it empirically instead of arguing. It took the engine's own operation and signed it with the subject's key: refused by the gate. With a key from outside the constitution: refused. Only an address the clients themselves list as a superadmin can produce that effect — so no lower-trust peer can cause it or benefit from it.

Then it did the thing I did not expect: it ran the fix the candidate proposed. Applied to the full documented rule ladder, the expired node reads guest, matches the onboarding rule, and gets promoted to a permanent role anyway. Carry the term forward instead, and two cycles after it lapses you are back at a permanent role — which is precisely the renewal loop that an earlier release removed rule-level expiry to eliminate, more than a year of releases ago.

An audit rediscovered, from scratch and without being told, why a design decision existed. That is what a written decision log buys you, and it is worth more to me than a report with nothing in it.

What did survive from that candidate was real, just smaller than advertised: two sentences of documentation that credited the engine with managing expirations it has never managed. Those are corrected.

What the Audit Sharpened

Four things were closed in the release that followed, each with a test that was red before its fix:

  • The gate now judges what a value carries, not only who signed it. On a node with an owner, a write by anyone but the owner must carry the owner's policy unchanged — owner, collaborators and the envelope table. Three lines of code, in the browser gate and its mirror in the Fallback Server.
  • A governance time objective is the engine's own observation, measured from when it first saw a node's current stamp — never the age of a stamp a peer signed for itself.
  • The priority path belongs to the constitution. A subject's write of its own role node never carries it.
  • Six demos render a peer's value as text, which is the rule the examples themselves state: no server sits between the writer and the reader.

The full report — method, what held, what was found, how each item was closed, and what this run explicitly did not cover — is published in docs/audits. The conformance suite came out of it larger than it went in: 84 tests in real browsers over real WebRTC, up from 76, and 107 gate verdicts, up from 87.

Audits Have a Home Now

Published audits live in docs/audits: one row and one report per run, with date, reviewer, scope and result. The README points there once and does not change again for an audit.

If you review GenosDB — as a researcher, a security team, or an organization evaluating it — I will list your report there, findings and all. The specification is public, the bundle is hashable, and the vectors run without a line of GenosDB code. Nothing about this arrangement requires you to believe me.

A Note on Where This Comes From

I have spent years building browser P2P databases — I wrote the official GUN documentation platform and contributed to that codebase before starting GenosDB. The reason this engine has a decision log, an invariant list and a battery that runs in real browsers over real WebRTC is that I have been on the other side of every one of those absences.

An audit of a serverless system is not a search for the hole in the wall. There is no wall. There are only receivers, each deciding alone, each having to be right without help. Testing that is harder than testing a server, and it is also the only way to find out whether "zero trust" is an architecture or a slogan.

⭐ Found this useful? Star GenosDB on GitHub — or spin it up in seconds: npm i genosdb.


This article is part of the official documentation of GenosDB (GDB). GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by Esteban Fuster Pozzi (estebanrfp).

📄 Whitepaper | overview of GenosDB design and architecture

🛠 Roadmap | planned features and future updates

💡 Examples | code snippets and usage demos

📖 Documentation | full reference guide

🔍 API Reference | detailed API methods

💬 GitHub Discussions | community questions and feedback

🗂 Repository | Minified production-ready files

📦 Install via npm | quick setup instructions

🌐 Website | GitHub | LinkedIn