fix(cache,serve): store-page landing provenance, single cache budget, unrecognised-dir accounting - #455
Draft
MichaelTaylor3d wants to merge 6 commits into
Draft
fix(cache,serve): store-page landing provenance, single cache budget, unrecognised-dir accounting#455MichaelTaylor3d wants to merge 6 commits into
MichaelTaylor3d wants to merge 6 commits into
Conversation
…es bondable A store's own page could make the node land attacker-chosen capsules Held, and so bondable with the operator's $DIG staked on a stranger's content. `/s/*path` and `POST /` are the same router on the same port, so a store page is same-origin with the RPC endpoint, and STORE_CSP grants store pages `script-src 'unsafe-inline'` with `connect-src 'self'` -- where 'self' IS that endpoint. The browser then truthfully reports `Sec-Fetch-Site: same-origin`, which mapped to FirstParty, folded to Local, and returned Announce. Because Announce also removes an existing marker, the same call could un-suppress a capsule that had been correctly relayed. The two-axis model could not express the fix: the operator's own read and attacker content served at /s/ are both (Local, FirstParty). Same-origin stopped being a trust signal the moment the node began serving untrusted content on its control origin, so this adds a THIRD provenance rather than a tighter reading of two. RequestProvenance::StoreServed covers every page-driven Sec-Fetch-Site value and folds to Peer. `none` (a user-initiated top-level navigation, unforgeable by page script) stays FirstParty, so opening a store in a browser still lands its capsule and the reshare flywheel survives. An absent header still means a non-browser CLI/SDK client and still lands. An unknown value now fails CLOSED. Deliberately not Referer-derived: a page controls its own referrer-policy and can strip the path or the header, so a Referer rule is bypassable by exactly the party it constrains. Sec-Fetch-* is browser-set and forbidden to script. Refs #450 Co-Authored-By: Claude <noreply@anthropic.com>
… count against it Two defects with one root: what the configured cap is a bound ON. #284 -- `cache_cap_bytes()` was read independently by both eviction paths over two different subtrees, so neither knew the other existed and a node configured for N bytes held close to 2N. `cache_budget()` now splits the one cap into a responses share (an eighth) and a modules share, so the two sweeps spend halves of one budget. A reserved share rather than 'modules take what responses leave' because the latter starves the response cache: its small regenerable windows always lose the race to ~135 MiB capsules. #265 -- the scan's hex64 filter governed BOTH the total measured AND the candidate set, so anything under <cache>/modules the node could not identify was invisible to the bound while consuming the disk the bound protects, and could grow without limit. Semantics chosen and now written down in the code: COUNT, never DELETE. Unrecognised bytes are charged against the modules share, so recognised capsules are evicted to compensate; the sweep never removes a file it cannot identify from a directory it does not exclusively own. used_bytes and cap_bytes now describe the same thing. Refs #284, #265 Co-Authored-By: Claude <noreply@anthropic.com>
…udget SPEC 21.8 gains the three-outcome Sec-Fetch-Site mapping (StoreServed, the unknown arm failing closed, and the explicit ban on deriving provenance from Referer/Origin), and states why 'none' must stay FirstParty. SPEC 7.10 states that cache_cap_bytes is ONE budget over the whole tree, split into a reserved responses share and a modules share, and that unrecognised bytes under modules are counted but never deleted. Refs #450, #284, #265 Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
`provenance_is_read_on_the_post_path` pinned `same-origin -> FirstParty` with the comment "a same-origin POST still lands". That is the dig-node#450 defect stated as a requirement: `/s/*path` and `POST /` share one router and port, and `STORE_CSP` grants `connect-src 'self'` -- where `'self'` IS the RPC endpoint -- so a store's own page reached `dig.getContent` as same-origin, landed its capsule `Held`, and staked this operator's $DIG on a stranger's content. The test asserted the behaviour the fix exists to remove, which is why it went RED on the fix rather than on the defect. Third instance of this shape found today. Now asserts the real contract, with the two controls that stop an over-correction: `none` (a user-initiated navigation, which script can never produce) stays FirstParty so the operator's own reads still land, and an unrecognised value fails CLOSED where it previously failed open. Refs #450 Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DRAFT — do not merge. The gate round has not returned. #450 is a security fix and wants the full security gate.
Closes #450
Closes #284
Closes #265
Cache-accounting, serving and landing-provenance batch. Three tickets landed; four in the original batch are not in this PR and are noted at the bottom with what was learned about each.
#450 — a store's own page could land attacker-chosen capsules bondable (SECURITY)
/s/*pathandPOST /are the same router on the same port, so a store page is same-origin with the RPC endpoint, andSTORE_CSPgrants store pagesscript-src 'unsafe-inline' 'unsafe-eval'withconnect-src 'self'— where'self'is that endpoint. The browser then truthfully reportsSec-Fetch-Site: same-origin, which mapped toFirstParty, folded toLocal, and returnedAnnounce: the capsule landedHeld, i.e. bondable, with the operator's $DIG staked on a stranger's chosen content. BecauseAnnouncealso removes an existing marker, the same call could un-suppress a capsule that had been correctly relayed.Shape decision (settled before building, §1.10): a THIRD provenance, derived from
Sec-Fetch-Sitealone.The two-axis model could not express the fix — the operator's own read and attacker content at
/s/are both(Local, FirstParty). Same-origin stopped being a trust signal the moment the node began serving untrusted content on its control origin.Why not the ticket's three options as written:
store_base_href,reroot_via_referer, dig-app, the extension). Too wide for a live exploit.Referer-derived is bypassable by exactly the party it constrains: a store page sets its own referrer-policy and can reduceRefererto origin-only or remove it entirely, and either makes the request look non-store. A one-line<meta>tag defeats it.connect-srcis insufficient, not merely costly:img-src 'self'alone lets a page issueGET /s/<other-store>/bigand land it, and CSP cannot express a per-path distinction.What the fix rests on instead:
/s/*is the only HTML surface this router serves (/,/health,/version,/openrpc.json,/.well-known/*,/ws,/ws/status,/verify/*are all non-HTML). So a page-driven request on this origin was, by construction, authored by store content.Sec-Fetch-Siteis browser-set and a forbidden header name, so page script can neither forge nor strip it.Sec-Fetch-SiteFirstPartynoneFirstPartysame-origin/same-site/ unknownStoreServed(new)Peercross-siteCrossSiteThe flywheel survives: the top-level navigation that opens a store is
noneand still lands the capsule; subresources are then served from that landed capsule. The stated cost is narrow — a subresource no longer lands on its own. The unknown arm now fails closed (it failed open). The read is never blocked either way.#284 — one budget enforced twice over two subtrees
cache_cap_bytes()was read independently by both eviction paths, so a node configured for N bytes held close to 2N.cache_budget()now splits the one cap into a responses share (an eighth) and a modules share (the rest); each sweep spends only its own.used_bytesandcap_bytesnow describe the same thing, so dig-app's storage warning can no longer show an impossible state.A reserved share rather than "modules take what responses leave", because the latter starves the response cache — its small regenerable windows always lose the race to ~135 MiB capsules.
#265 — unrecognised directories escaped the cap
Semantics chosen and now documented in the code: COUNT, never DELETE. The sweep does not exclusively own
<cache>/modules, and deleting files it cannot identify is the class of thing that is fine until the once it is not. Unrecognised bytes are charged against the modules share, so recognised capsules are evicted to compensate.unrecognised_module_bytes()measures the subtree and subtracts what the scan would return, so the two halves partition by construction rather than via two filters kept in agreement by hand — which is how the original defect arose.Blast radius
.gitnexusis stale for this repo, so this was done withgit grep+ direct reads rather thanimpact— stated per §2.0, which permits the fallback but requires saying so.RequestProvenance— apubenum gaining a variant. Every match site enumerated and checked:download.rs(landing_origin),dispatch.rs(holder_claim_for_landing, plus its two call sites),capsule_store.rs,push_capsule.rs,lib.rs(construction sites, allFirstParty),server.rs(provenance_for). Onlylanding_originmatches on the enum exhaustively; every other site constructs or forwards.cargo check -p dig-node-service --all-targetsclean.from_sec_fetch_site— one production caller (server.rs:988), feeding both the/s/andPOST /doors.cache_cap_bytes— the two eviction sweeps (both changed) plus the reporting sites (dispatch.rs,/health), which are unchanged and now consistent.scan_cached_modules— one production caller (evict_modules_locked), now joined byunrecognised_module_bytes.Evidence
cargo test -p dig-node-core --lib— 1028 passed, 0 failed.cargo test -p dig-node-service --test content_serve— 15 passed, 0 failed.cargo clippy -p dig-node-core -p dig-node-service --all-targets -- -D warnings— exit 0.cargo fmt --all -- --check— exit 0.Revert-proofs, each committed first and reverted by file copy (never
git checkout):Sec-Fetch-Sitemapping onlyStoreServed => origin)holder_claim_testsPASSEDcache_cap_bytes()one_budget_bounds_both_cache_subtrees_togetherFAILEDunrecognised = 0unrecognised_bytes_under_modules_are_counted_but_never_deletedFAILEDThe second row is the one worth reading: the four
#436tests are green with the#450bug present, exactly as the ticket predicted, which is why the new test is written againstStoreServedover aLocaltransport rather than against a landed marker further down.Fixture notes, since this family keeps producing false greens:
SemVer
0.189.0 to 0.190.0 (minor). A
pubenum without#[non_exhaustive]gains a variant, which is breaking for a downstream exhaustive match, and landing behaviour changes — under 0.x that is a minor.Unverified / not done
same-originhere. The fix is strictly safer than the status quo under either answer, since it only ever restricts landing.RequestProvenanceis not#[non_exhaustive]. Marking it would prevent the next variant being breaking, but is itself breaking now. Left alone deliberately.serve_cached_responsestill touches on every hit, so inbound peer traffic can still order this node's eviction. Routing<cache>/responsesthroughdig_store_cache::EvictionPolicy/dig_sex::TieredPolicyis the survivor the ticket names and is a larger change. Noted on the ticket.dig-node-coredoes not have — and a test that points at an unreachable upstream passes against the old code too.