Skip to content

protect: accept live rule updates atomically; reject rule_id-less whitelists - #121

Merged
patchstackdave merged 1 commit into
mainfrom
hardening/atomic-bundle-acceptance
Aug 13, 2026
Merged

protect: accept live rule updates atomically; reject rule_id-less whitelists#121
patchstackdave merged 1 commit into
mainfrom
hardening/atomic-bundle-acceptance

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

An external round-2 review caught that our own validation could make a bad update worse than no update — the sharpest kind of finding, since the hardening was meant to reduce that risk.

The problem

validateBundle dropped invalid rules and the remaining bundle was then written to cache as a success. So:

  • a server/proxy error producing a syntactically valid envelope whose rules are all invalid, or
  • an oversized bundle whose important rules fall past the 5000-rule cap,

could replace known-good policy with partial or empty policy — and cache that loss.

The fix: all-or-nothing for live updates

If any rule or whitelist fails validation, the entire update is rejected:

  • last-known-good is retained (cache → bundled → empty, in that order),
  • every rejection is reported (onRuleRejected with accepted: false, plus an onError summary naming the count and first offenders),
  • the cache is not overwritten.

Dropping individual bad rules is still correct for a bundle we already trust (a cache entry, the bundled fallback) — the atomic rule applies to fresh remote responses. acceptPartialBundle: true opts back into the previous behaviour.

Global whitelists

A whitelist with no rule_id applies to every rule, so one tripped (attacker-reachable) condition disables the whole firewall for that request. It previously only warned; it is now rejected unless allowGlobalWhitelists is set.

Tests

tests/protect/atomic-bundle.test.ts proves the guarantee that matters — after a bad update the app is still protected by the previous ruleset and the cache file is byte-identical — plus: rejections are reported and nothing is cached; fallback prefers the bundled ruleset over empty policy; partial acceptance works only when opted in; a rule_id-less whitelist is rejected. 759 tests green, typecheck clean.

Deliberately not changed

The 5 s boot budget. The review suggested 1–2 s; we're keeping 5 s. It already bounds what was a 30 s startup exposure, and a 1–2 s default would push legitimately slow-but-healthy networks onto stale/bundled rules more often — a protection trade-off in the other direction. Hosts can lower it via bootTimeoutMs.

…telists

Round-2 review finding: our own validation could make a bad update WORSE than no
update. Invalid rules were dropped and the remaining bundle was then written to cache
as a success — so a malformed-but-well-formed-envelope response, or an oversized bundle
whose important rules fall past the 5000-rule cap, could replace known-good policy with
partial or empty policy AND cache that loss. Validation must not worsen the unsigned-
bundle threat it was added to reduce.

A LIVE update is now all-or-nothing: if any rule or whitelist fails validation, the whole
update is rejected, last-known-good is retained (cache → bundled → empty, in that order),
every rejection is reported (`onRuleRejected` with `accepted: false`, plus an `onError`
summary), and the cache is NOT overwritten. Dropping individual rules is still fine for a
bundle we already trust (a cache entry, a bundled fallback). Opt back in to partial
acceptance with `acceptPartialBundle: true`.

Also: a whitelist with no `rule_id` applies to EVERY rule, so one tripped condition
disables the whole firewall for that request. Previously it only warned; it is now
rejected unless `allowGlobalWhitelists` is set.

Documents that `onSkip` is a trusted server callback whose `detail` must not be forwarded
to clients.

Not changed: the 5s boot budget. It already bounds what was a 30s startup exposure, and a
1-2s default would push legitimately slow-but-healthy networks onto stale/bundled rules
more often — a protection trade-off in the other direction. Hosts can lower it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Aug 13, 2026

Copy link
Copy Markdown

Implements atomic live updates and rejects ruleless whitelists securely.

🎯 Quality: 100% Elite · 📦 Size: Medium

📈 This month: Your 54th PR — above team average · Averaging Excellent

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit 5c36e9f into main Aug 13, 2026
5 checks passed
@patchstackdave
patchstackdave deleted the hardening/atomic-bundle-acceptance branch August 13, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants