mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-02 03:21:01 +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
@@ -49,6 +49,10 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
/** @type {Error|null} */
|
||||
this.error = null;
|
||||
this._ready = false;
|
||||
// Increments on every start, so a start that was superseded mid-flight can
|
||||
// tell it lost the race and clean up after itself instead of overwriting
|
||||
// the newer runtime and leaking its own sockets.
|
||||
this._generation = 0;
|
||||
|
||||
// A promise as well as an event. The event fires during element upgrade,
|
||||
// which for a page that calls defineBitGateElements() after parsing is
|
||||
@@ -92,8 +96,13 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
attributeChangedCallback(name, before, after) {
|
||||
// Only rebuild once connected and actually changed; the initial attribute
|
||||
// pass would otherwise construct several runtimes during parsing.
|
||||
if (this._ready && before !== after) {
|
||||
//
|
||||
// `stop()` clears the ready flag, so it is restored here — otherwise the
|
||||
// first attribute change would leave the guard permanently false and every
|
||||
// later change would be silently ignored.
|
||||
if (this._ready && this.isConnected && before !== after) {
|
||||
this.stop();
|
||||
this._ready = true;
|
||||
void this.start();
|
||||
}
|
||||
}
|
||||
@@ -114,6 +123,10 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
* whatever cached state exists.
|
||||
*/
|
||||
async start() {
|
||||
const generation = (this._generation += 1);
|
||||
/** @type {any} */
|
||||
let transport = null;
|
||||
|
||||
try {
|
||||
const relays = this.relayUrls;
|
||||
const policyName = this.getAttribute("policy") ?? "admin-only";
|
||||
@@ -128,12 +141,21 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
throw new Error("bitgate-provider requires a relays attribute");
|
||||
}
|
||||
|
||||
this.transport = createRelayTransport(relays);
|
||||
transport = createRelayTransport(relays);
|
||||
|
||||
// A newer start began while this one was resolving its configuration.
|
||||
// Close what was just opened rather than clobbering the newer runtime.
|
||||
if (generation !== this._generation) {
|
||||
transport.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.transport = transport;
|
||||
this.runtime = createBitGate({
|
||||
applicationId: this.getAttribute("application") ?? globalThis.location?.hostname ?? "bitgate",
|
||||
namespace: this.getAttribute("namespace") ?? "bitgate",
|
||||
root: this.getAttribute("root") ?? undefined,
|
||||
transport: this.transport,
|
||||
transport,
|
||||
policy,
|
||||
storage: createLocalStorage(),
|
||||
now: () => Math.floor(Date.now() / 1000),
|
||||
@@ -156,6 +178,10 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
}),
|
||||
);
|
||||
|
||||
if (generation !== this._generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runtime.loadAdministrativeState();
|
||||
|
||||
// With a viewer known, build the trust graph without the host having to:
|
||||
@@ -168,6 +194,10 @@ export class BitGateProvider extends globalThis.HTMLElement {
|
||||
|
||||
this.dispatchEvent(new CustomEvent("bitgate:loaded", { bubbles: true, composed: true }));
|
||||
} catch (error) {
|
||||
transport?.close?.();
|
||||
if (generation !== this._generation) {
|
||||
return;
|
||||
}
|
||||
this.error = /** @type {Error} */ (error);
|
||||
this.setAttribute("data-error", String(error));
|
||||
this._rejectReady?.(error);
|
||||
|
||||
Reference in new issue
Block a user