mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-06 05:21:10 +00:00
fix(security): close every finding from the audit
Critical - Any pubkey could publish a kind:30078 roles document and become root of a deployment, granting itself every capability and denying anyone. Roles are now accepted only from the configured root, compared against the constructor value rather than current authority state so a bad ingest cannot move the goalposts. Reproduced end to end before the fix; regression test added. - Administrative state now fails closed without signature verification. Without it, event.pubkey is an unauthenticated claim, so an authorship check alone proves nothing — a relay can put the root's key on an event the root never wrote. trustUnsignedEvents opts in for local and test use. High - Report categories could evade moderation entirely: resolveThresholds used bracket truthiness, so "constructor" resolved to the Object constructor (all gates undefined) and "__proto__" to Object.prototype, both bypassing the default threshold. Own-property lookups and null-prototype tables throughout core — policy, evaluator, evidence, authority, presets. - Live subscriptions bypassed signature verification entirely; an attacker only had to wait for the subscription rather than the load. - The default administrative query asked relays for every kind:30078 and kind:30000 event in existence, with no author, identifier, or limit. Now filtered to governance identifiers with a bounded limit. Medium - Cached state was trusted as authority: a cache naming a different root is now rejected, so same-origin tampering cannot persist a takeover. - Provider attribute changes silently stopped working after the first one, and concurrent starts leaked a transport. Generation-guarded. - Unbounded growth in the report store, mute store, decision cache, and the transport's per-subscription dedupe set — all now capped. Also restores the on-wire `d` identifier to <namespace>:governance:<scope>:v1. The BitGate rename replaced "governance:" too broadly and silently changed the event format; both encoder and parser changed together, so round-trip tests never noticed. The identifier names the kind of document, not the library. Adds docs/security.md and 21 regression tests reproducing each finding. 832 tests.
This commit is contained in:
1 parent
7a45f1cf3e
commit
94ef6a72f5
21 files changed
+788
-54
No files matched your search
@@ -250,6 +250,8 @@ describe("contribution round-trip", () => {
|
||||
transport: createMemoryTransport(),
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
root: ROOT,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
fresh.admin.setRoles({
|
||||
root: ROOT,
|
||||
|
||||
@@ -76,6 +76,7 @@ function makeRuntime(transport) {
|
||||
transport,
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
runtime.setViewer(VIEWER);
|
||||
return runtime;
|
||||
@@ -238,6 +239,7 @@ describe("contact list loading", () => {
|
||||
transport,
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
expect(await runtime.loadContacts()).toBe(0);
|
||||
});
|
||||
@@ -275,6 +277,7 @@ describe("the viewer's own private mutes", () => {
|
||||
signer: signerWithNip44(),
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
runtime.setViewer(VIEWER);
|
||||
|
||||
@@ -292,6 +295,7 @@ describe("the viewer's own private mutes", () => {
|
||||
signer: signerWithNip44(),
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
runtime.setViewer(VIEWER);
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ function makeRuntime({ storage, transport, verifySignature, root = ROOT } = {})
|
||||
now: () => NOW,
|
||||
verifySignature,
|
||||
root,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -331,13 +332,13 @@ describe("community sources", () => {
|
||||
});
|
||||
|
||||
const merged = await runtime.loadCommunitySources([
|
||||
{ curator: CURATOR, identifier: "app:bitgate:user-deny:v1", kind: CANONICAL_KIND },
|
||||
{ curator: CURATOR, identifier: "app:governance:user-deny:v1", kind: CANONICAL_KIND },
|
||||
]);
|
||||
|
||||
expect(merged).toBe(1);
|
||||
expect(runtime.admin.state.userDeny.has(`user:${CREATOR}`)).toBe(true);
|
||||
expect(runtime.admin.state.communitySources.get(`user:${CREATOR}`)).toEqual([
|
||||
`${CURATOR}:app:bitgate:user-deny:v1`,
|
||||
`${CURATOR}:app:governance:user-deny:v1`,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -357,7 +358,7 @@ describe("community sources", () => {
|
||||
runtime.admin.setRoles({ root: ROOT, actors: { [CURATOR]: ["curator"] } });
|
||||
|
||||
await runtime.loadCommunitySources([
|
||||
{ curator: CURATOR, identifier: "app:bitgate:event-deny:v1", kind: CANONICAL_KIND },
|
||||
{ curator: CURATOR, identifier: "app:governance:event-deny:v1", kind: CANONICAL_KIND },
|
||||
]);
|
||||
|
||||
expect(runtime.admin.state.eventDeny.size).toBe(0);
|
||||
|
||||
@@ -72,6 +72,8 @@ function makeRuntime(events = []) {
|
||||
transport,
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
root: ROOT,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
return { runtime, transport };
|
||||
}
|
||||
@@ -132,7 +134,9 @@ describe("construction", () => {
|
||||
it("changes its storage key when the root authority changes", () => {
|
||||
const { runtime } = makeRuntime();
|
||||
const before = runtime.storageKeyFor("admin");
|
||||
runtime.admin.setRoles({ root: ROOT, actors: {} });
|
||||
// Rotating the root must invalidate the cache namespace so a new
|
||||
// administration cannot inherit the previous one's stored state.
|
||||
runtime.admin.setRoles({ root: CREATOR, actors: {} });
|
||||
expect(runtime.storageKeyFor("admin")).not.toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -266,6 +270,8 @@ describe("loadAdministrativeState", () => {
|
||||
transport,
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
root: ROOT,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
|
||||
await runtime.loadAdministrativeState({ authors: [MODERATOR] });
|
||||
@@ -284,6 +290,8 @@ describe("active targets and subscriptions", () => {
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
chunkSize: 2,
|
||||
root: ROOT,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
|
||||
runtime.setActiveTargets([
|
||||
@@ -305,6 +313,8 @@ describe("active targets and subscriptions", () => {
|
||||
transport,
|
||||
policy: POLICY,
|
||||
now: () => NOW,
|
||||
root: ROOT,
|
||||
trustUnsignedEvents: true,
|
||||
});
|
||||
|
||||
runtime.setActiveTargets([
|
||||
|
||||
Reference in new issue
Block a user