mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-05 04:51:00 +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
@@ -231,10 +231,18 @@ export class TrustGraphStore extends Emitter {
|
||||
* NIP-56 reports, aggregated per target.
|
||||
*/
|
||||
export class ReportStore extends Emitter {
|
||||
constructor() {
|
||||
/**
|
||||
* @param {Object} [options]
|
||||
* @param {number} [options.maxTargets] - Ceiling on distinct targets retained
|
||||
*/
|
||||
constructor({ maxTargets = 0 } = {}) {
|
||||
super();
|
||||
/** @type {Map<string, Map<string, Map<string, number>>>} reports[targetKey][reporter][category] = createdAt */
|
||||
this.reports = new Map();
|
||||
// Relays push reports for targets this viewer may never look at again.
|
||||
// Without a ceiling a long-running session accumulates every target ever
|
||||
// mentioned.
|
||||
this.maxTargets = maxTargets;
|
||||
this.fingerprint = fingerprint({});
|
||||
}
|
||||
|
||||
@@ -275,6 +283,14 @@ export class ReportStore extends Emitter {
|
||||
}
|
||||
|
||||
byCategory.set(report.category, createdAt);
|
||||
|
||||
if (this.maxTargets > 0 && this.reports.size > this.maxTargets) {
|
||||
const oldest = this.reports.keys().next().value;
|
||||
if (oldest !== undefined && oldest !== targetKey) {
|
||||
this.reports.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
this.#emitChange(targetKey);
|
||||
}
|
||||
|
||||
@@ -333,11 +349,14 @@ export class TrustedMuteStore extends Emitter {
|
||||
* @param {Object} [options]
|
||||
* @param {number} [options.windowSeconds] - Mutes older than this are pruned
|
||||
* @param {() => number} [options.now] - Injected clock, unix seconds
|
||||
* @param {number} [options.maxLists] - Ceiling on retained lists; 0 disables eviction
|
||||
*/
|
||||
constructor({ windowSeconds = 0, now = () => 0 } = {}) {
|
||||
constructor({ windowSeconds = 0, now = () => 0, maxLists = 0 } = {}) {
|
||||
super();
|
||||
this.windowSeconds = windowSeconds;
|
||||
this.now = now;
|
||||
/** Ceiling on retained lists; 0 disables eviction. */
|
||||
this.maxLists = maxLists;
|
||||
/** @type {Map<string, { owner: string, updatedAt: number, entries: Array<{ pubkey: string, category?: string }> }>} */
|
||||
this.lists = new Map();
|
||||
this.fingerprint = fingerprint({});
|
||||
@@ -372,6 +391,14 @@ export class TrustedMuteStore extends Emitter {
|
||||
updatedAt: list.updatedAt,
|
||||
entries: list.entries,
|
||||
});
|
||||
|
||||
if (this.maxLists > 0 && this.lists.size > this.maxLists) {
|
||||
const oldest = this.lists.keys().next().value;
|
||||
if (oldest !== undefined && oldest !== list.owner) {
|
||||
this.lists.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
this.#emitChange(Array.from(affected));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user