Threat table

All 45 threats in the model, each with Weir's answer, the requirement behind it and the test that proves it.

Rendered at build time from docs/06-threat-model.md, section 3, which is the normative source. Link to a row with its ID, for example /security/threats/#t-13.

Rows are grouped by family, so IDs are not in numeric order within the page: T-45 sits in the first table, beside the poisoning threats it belongs with.

Each row: the attack, where it comes from, Weir's answer, the requirement or ADR, and the test that proves it.

Poisoning and deception

IDThreatAnswerRefsTest
T-1Unkeyed header poisoning: origin reflects or reacts to a header (for example X-Forwarded-Host, User-Agent, X-Forwarded-Port, Accept, Origin) that the cache does not key (Kettle 2018, 2019)strict forwarding: unkeyed headers do not reach the origin on cacheable requests, except the fixed list in FR-FWD-1 and D29 (Authorization, Cache-Control, Pragma, trace fields), whose effect T-31 and T-40 handle; the cache directives and the trace fields go only in a bounded byte set, so none can carry markup or a payload into a response stored for othersD4, P2, FR-FWD-1, ADR-4TestForwardEqualsKey, TestKettleUserAgent, TestUnkeyedHeaderNotForwarded, TestKeyedInputsSplitEntries
T-2Unkeyed or cloaked query parameters: parameter excluded from the key but forwarded; ; versus & parsing differences (Kettle 2020)excluded parameters are removed from the forwarded query; kept segments are forwarded byte-for-byte, so any origin parse of them is also a parse of keyed bytesP2, FR-KEY-5TestQueryDropRemovesFromForward, TestParameterCloakingSemicolon
T-3Key injection by delimiter collision (Akamai __ example, Kettle 2020)tagged, length-prefixed canonical encoding hashed with SHA-256FR-KEY-1, ADR-3TestKeyEncodingInjective (property), FuzzKeyEncodingInjective
T-4Per-client transport headers: adapters or proxies add X-Forwarded-For (or similar) to forwarded requests, and the origin varies content on it (geo, A/B)Weir cannot see headers added after it. Documented adapter rule (03-hld.md §5); Caddy spec requires origins not to vary unkeyed on these, or to key a derived dimension (Phase 3)08 §6manual review item
T-5Fat GET: body on a GET changes the response but not the key (Kettle 2020)the forwarded request for a cacheable GET/HEAD never has a bodyFR-FWD-1TestFatGETBodyDropped
T-6Path normalization discrepancies and web cache deception: cache and origin disagree on what /account;x.css, /a/..%2fhome, %2F mean (Doyhenard 2024)Weir keys and forwards the exact path bytes (optional RFC 3986 normalization applied to both). Weir has no "cache by file extension" or "cache by directory" rules: storability comes only from origin headers. A raw # in path or query, or ? in path, is rejected, so no origin reads a fragment or query the key did not seeFR-KEY-4, FR-STO-8, FR-VAL-1TestPathForwardedByteExact, TestNoExtensionBasedCaching, TestFragmentInTargetRejected, TestAbsoluteFormFragmentRejected, FuzzRequestFrom
T-7Error-page poisoning: attacker makes the origin return an error (WAF 403, Range: bytes=cow 400, Transfer-Encoding 501) that is stored for everyone (Kettle 2019)400 and 403 are not in the default storable set. A cacheable fetch never carries Range: a Range request with no usable entry is passed through with its Range, and its response is neither stored nor turned into a marker. Transfer-Encoding and other hop-by-hop fields are stripped, so a client cannot provoke the 501 (which is storable by default, as RFC 9110 lists it). Unkeyed headers are not forwarded at allFR-STO-2, FR-FWD-1, FR-FWD-2, FR-SRV-5TestRangeGarbageNotPoisoning, TestErrorStatusesNotStored, TestUnforwardable, TestClassifyPassForward
T-8Private data leakage: Set-Cookie, Authorization-bearing, or private responses stored and sharedRFC 9111 §3.5 enforced, and only a valid s-maxage, or public or must-revalidate in a well-formed field (no argument, no unclosed quote), grants its permission; Set-Cookie responses not stored unless stripped by explicit config; private never stored; Vary: Cookie and Vary: Authorization not stored without explicit allow; requests with Authorization do not coalesce; bypass cookie rules match as widely as a lenient origin parser, so a reshaped session cookie cannot turn a personalized response into a shared one under ForwardAllFR-STO-4..6, FR-KEY-9, FR-COA-8, FR-BYP-1TestSetCookieNotStored, TestAuthorizationRules, TestAuthorizationNeedsValidSMaxage, TestAuthorizedNotCoalesced, TestBypassNeverStored, TestBypassCookieEvasion, FuzzBypassed, TestBypassedCookieShapes, TestAuthorizationNeedsBarePublic, TestFreshenRefusesUnstorable304
T-9Stale purge: an entry that should be purged is served because the epoch lookup failedmemory store epoch lookups cannot fail; remote stores fail open with an event. Chosen because failing closed would turn a store blip into a full miss storm. Operators who need fail-closed purges should also hard-purge by All after an incident04-lld.md §6.3TestEpochLookupErrorEmitsEvent
T-10Purge race: a fetch in flight during a purge stores pre-purge content afterwardsepochs compare against RequestTime, not store timeFR-PRG-7TestPurgeDuringInflightFetch
T-45In-process origin reads unkeyed context: HandlerOrigin (and the Caddy nextOrigin) hands the handler the flight creator's context values (FR-COA-9), so an identity or variable that upstream middleware put there can shape a response that is then stored and sharedWeir cannot see context values, so it cannot key them. A handler behind Weir must derive per-user output only from the forwarded request, or the route must bypass the cache; the same rule as R-1. TransportOrigin sends only the forwarded request and is not affectedFR-COA-9, INV-1none (operator rule; the weirhttp and Caddy guides state it, R-6)

Availability

IDThreatAnswerRefsTest
T-11Cache busting by unique query strings on one path (Ferretti and Ghini 2012)per-partition cap bounds concurrent origin work for that path, and the queued waiters for it (at most a quarter of the queue), so the flood cannot fill the shared queue; miss-rate anomaly event; S3-FIFO keeps one-hit entries out of the main queueFR-LIM-3, FR-MR-*, ADR-6, ADR-8TestRandomQueryFloodBounded, TestPartitionFairness, TestLimiterPartitionQueueCap, TestMissRateAnomaly, TestMissRateThrottle, TestMissRateThrottleHoldsAcrossWindows, TestMissRateThrottleSkipsLateReport, TestMissRateIgnoresFreeRequests, TestMissRateCountsOwnFetchesOnly, TestS3FIFOScanResistance
T-12Cache busting across many distinct pathsglobal cap bounds origin load; legitimate misses on other paths may shed. Residual: this is rate-limiting territory (for example caddy-ratelimit in front of Weir)ADR-8TestPathFloodOriginBounded
T-13Malformed keyed header forcing bypass (CVE-2024-35296, Accept-Encoding)normalizers map any malformed value to one canonical bucket, both in key and forward; never a bypass, never an error. The Cookie limit counts keyed pairs only, so a large unkeyed cookie cannot push a keyed one to absentFR-VAL-3, §5.2.3TestCVE202435296, FuzzAcceptEncoding, TestKeyedCookieLimitCountsKeyedPairs, TestNormalizeHeader, TestClassifyKeyedHeaders
T-14Client revalidation directives as a bypass (Cache-Control: no-cache, Pragma: no-cache)ignored by defaultD5, FR-SRV-8TestClientNoCacheIgnored
T-15Vary explosion: origin varies on User-Agent, attacker cycles values. Variant confusion: two renderings of a Vary field that the origin answers differently (two lines vs one, a comma in a quoted string, a non-canonical name) share a variantvariants capped per primary key; over the cap new variants are not stored (answered, not cached). Vary values keyed exactly as forwarded and request names canonicalized before keying, so the variant always reflects what the origin sawFR-KEY-10, FR-KEY-11, INV-1TestVaryOverflow, TestVaryKeysExactLines, TestVaryNonCanonicalRequestKey
T-16Breaker tripping: attacker triggers origin errors to open the breaker site-wideonly gateway failures count, by ratio with minimum volume; 500 excludedADR-7, FR-CB-2TestBreaker500DoesNotTrip, TestBreakerNeedsVolume
T-17Negative-cache poisoning: attacker-induced failure cached for everyonenegative entries only for gateway failures, status-only, 2 s, and the failing request's inputs are all keyed (strict forwarding), so the negative entry sits on the attacker's own keyFR-NEG-*, D4TestNegativeScopedToKey
T-18Slow-reader slot pinning on streamed responsesstreamed fetches release their slot at headers; stream bounded by origin timeoutFR-LIM-1TestSlowReaderDoesNotPinSlots
T-19Stuck leader stalls a herdleader aging, follower wait bound, stale fallbackFR-COA-3, FR-COA-4TestCoalesceStuckLeader
T-20Background refresh amplificationrefresh only on request, one per key, background class cannot use the foreground reserve and never queuesFR-STL-6, FR-LIM-4TestRefreshNeverExceedsReserve
T-21Memory exhaustion via large bodies, many headers, many groupsobject size cap including headers; Entry.Size charges every header name and value its in-memory overhead, so tiny values cannot hold many times their accounted bytes; the Valkey store reads at most MaxObjectBytes + 1 bytes of a value; group count and length caps; transient buffering bounded by MaxConcurrent × MaxObjectBytesFR-STO-9, FR-STO-10, NFR-4TestOversizedStreamedNotBuffered, TestTransientBodyMemoryBounded, TestEntrySize, TestGetOversizedValueIsMiss
T-22Shard grinding: attacker chooses inputs so keys land in one memory-store shardshard index from maphash with a per-process seedADR-3, 05 §5.2TestShardDistributionAdversarial
T-23Epoch growth via Cache-Group-Invalidation (A3)groups bounded per response; soft and invalid epochs live in a fixed-size sketch, so volume cannot grow memory. Residual: one response can write 32 soft epochs, so about 1 900 unsafe requests answered that way load the soft plane as 60 000 invalidations do (05 E-8); entries in no group go soft-stale too. The cost is early refreshes, in the background inside a stale-while-revalidate window and bounded by the limiter (T-28); an entry with no such window is revalidated before it is served. RFC 9875 §2 requires at least 32 groups per field, so the count is not capped lower. EvPurge{group} carries the count per response for alerting, and CacheGroups.Ignore removes the attack05 §4.4TestInvalidationFloodBounded, TestGroupInvalidationFloodStaysServable
T-31Marker and negative-entry poisoning: a client sends Authorization: junk or Cache-Control: no-store on a cold URL; the origin's response (401, or anything under request no-store) is not storable, and a hit-for-miss marker or negative entry would then switch coalescing or caching off for that URL for everyonemarkers and negative entries are written only for response-driven failures on requests without Authorization or request no-store, and never after a forwarded Forward.Allow field, an unkeyed Cache-Control or Pragma field (forwarded only as a plain directive list of at most 256 bytes, FR-FWD-1, and still outside the key, so an origin that answers only-if-cached with 504 would otherwise land on the shared key), or under ForwardAll; neither ever replaces a stored responseFR-STO-12, FR-NEG-4TestMarkerNotFromAuthorizedRequest, TestMarkerNotFromRequestNoStore, TestNoMarkerAfterUnkeyedInput, TestNegativeNotFor500, TestClassifyKeyedHeaders
T-32Tenant cache pollution (A3): a tenant requests its own unique URLs twice each so they reach the S3-FIFO main queue and evict other tenants' hot entriesper-owner byte quota per shard; over-quota inserts evict only the same owner's entries or are declinedFR-FAIR-2TestOwnerQuotaIsolatesTenants
T-33Snapshot resurrection: a purge issued while a node was down (or a stale snapshot file) brings purged content back after restartloaded entries are invalid as of load time (revalidated, never served stale); hard epochs persist; a lost record that may have held one makes the loader write a global hard epoch; the file is deleted after load, and a failed delete also writes it; incomplete files are ignored and deleted when they carry the magic; request times after load time are clamped. Tampering needs write access to the host, which is out of scope; CRC only detects corruptionFR-SNP-1..3TestSnapshotLoadIsSoftStale, TestSnapshotHardEpochSurvives, TestSnapshotCorruptRecordsSkipped
T-34Targeted-field leak: origin sets CDN-Cache-Control globally and marks per-user pages private only in Cache-Control; RFC 9213 would have the cache ignore privateprivate, no-store, no-cache in Cache-Control still apply (stricter than RFC 9213)FR-TCC-3TestTargetedFieldKeepsPrivate
T-35Variant spoofing: client sends Weir-Variant or edits the assignment cookie to pick a variantheader always replaced (names that differ only by _ vs - are also removed, header name may not contain _ or collide with a framing/cookie header); cookie HMAC-signed with rotation, keys under 32 bytes never sign or verify, one canonical spelling per cookie; the header name may not be a field the forward step drops or filters; the visitor cookie is stripped on every path and never keyed10 §2 E2, §5Phase 3 tests
T-36Downstream variant mixing: a CDN in front of Weir caches variant responses (or the assignment Set-Cookie) under a key without the variantexperiment responses rewritten to private for downstream unless the operator declares variant-aware downstream caches10 §2 E7TestDownstreamRewrittenPrivate, TestDownstreamVariantAwareKeepsDirectives, TestSetCookiePerClientOnly, TestSetCookieNeverStored
T-37Range amplification: range requests on many URLs trigger full-object fetchesrange requests never cause foreground full fetches; background fill only when the origin's 206 declares a total size within MaxObjectBytes, one flight per key, background class (no queueing, never takes the foreground reserve), no fill for HEAD, credentialed or no-store requests or marked keys, and an over-size fill leaves a marker. Residual: a fill that fails (5xx, error) repeats per Range request, bounded by the Background limit and counted by the breakerFR-RNG-4TestRangeMissBackgroundFillBounded
T-38Multi-node purge gap: with per-node memory stores, a purge sent to one node leaves others serving the contentA memory-store site is single-node (D17); several nodes require the Valkey store (shared entries and epochs), configured as in runbook §8: one prefix per cache, the same forward, stale and key settings on every node, clocks within MaxClockSkewD17 (lifted for Valkey, P25-07b)TestE2ETwoNodePurge (two Caddy processes, one Valkey); the deployment rules in runbook §8
T-39Slow-upload slot pinning: many requests with slowly streamed bodies hold origin slotsbodies use a separate upload pool, so cacheable misses and bodyless requests are unaffected; adapters must bound body size and read time. Residual: an upload flood can still shed other uploadsFR-LIM-7TestSlowUploadsDoNotStarveMisses
T-40Trace-header echo: an origin reflects X-Request-Id or traceparent into a cacheable body, making it an unkeyed inputforwarded by default for tracing; validated format, length and byte set (letters, digits and `-_.:/=+@,;*~!), at most 32 tracestatelines, none empty, no.., so neither field can carry a payload or a field count that a WAF or origin would reject onto the shared key (T-31); documented as origin misuse; operators can set Forward.NoTraceHeaders`FR-FWD-6
T-41Drip-feeding origin: an origin (or something in front of it) sends a cacheable body one byte at a time to hold limiter slotsbuffered body reads stay under the total origin timeoutFR-TMO-1TestDripOriginReleasesSlot
T-42Forgotten incident mode: ModeStaleOnError or ModeBypass left on for daysmandatory expiry of at most 24 h; modes not persisted; events and logs on every changeFR-MODE-1TestModeExpires
T-43Memory overcommit: several default-sized stores in one process each take 40% of GOMEMLIMITthe Caddy adapter splits the budget across stores (each new one takes half of the unclaimed part, not an even share); library users constructing several engines are warned in docsFR-MEM-1TestMemorySizingSplit (Caddy)
T-44Upgrade smuggling: a WebSocket over HTTP/2 extended CONNECT carries no Upgrade header and slips past upgrade detectionCONNECT in any form is rejected by Serve and routed around by adapters. The one Upgrade served normally is a lone h2c token; Upgrade and HTTP2-Settings are hop-by-hop, so the origin sees a plain request and key and forward stay equal. h2c with any other token is still an upgradeFR-UPG-1TestConnectRejected, TestH2CUpgradeServedNormally, TestH2CKeyedLikePlainRequest
T-24Unbounded host keyspace: arbitrary Host values create entriesadapter routes by host before Weir (Caddy site blocks); inside Weir, new keys land in the small S3-FIFO queue and churn only itADR-6covered by T-11 tests
T-28Group-invalidation amplification: attacker makes cheap unsafe requests whose responses carry Cache-Group-Invalidation for a large, hot group, forcing every entry in it to revalidate before it can be servedgrouped invalidation is applied as a soft purge, so SWR and SIE windows still apply and revalidation is background where the origin allows it; revalidations are coalesced and limiter-bound. This covers invalidation by the field; the URI flood of T-29 cannot make a group revalidate on a store with SharedTagEpochs (05 E-12)FR-INV-2TestGroupInvalidationIsSoft
T-29Invalidation flood: many unsafe requests to distinct URIs (POST /x?r=<random> answered with 2xx) each create a MUST-invalidate epoch (RFC 9111 §4.4). An exact epoch table either grows without bound or, if capped with a global fallback, lets ~100 000 cheap requests invalidate the whole cachefixed-size max-timestamp sketch for soft and invalid epochs: memory constant, never under-invalidates, over-invalidation degrades gradually with volume (about 4% of entries without groups at 60 000 invalidations per entry lifetime at defaults, more for entries in many groups, 05 E-8). Entries carry no origin tag, so a false positive on one never covers an origin. Group tags are looked up through SharedTagEpochs, which skips the invalid plane for them (05 E-12), so the flood cannot make a group revalidate. Residual: a store without that capability keeps the 4% per-group chance (once per group, bounded by coalescing and the limiter)05 E-7..E-9, E-12TestInvalidationFloodBounded, EpochNeverUnderInvalidates, TestSharedTagsSkipInvalidPlane, TestInvalidationFloodLeavesGroupsServable
T-30Origin clock skew: an origin whose Date runs behind makes responses look old on arrival (apparent_age), turning the cache off; one running ahead is harmlessage uses age_value + response_delay only (RFC 9111 §4.2.3 permits it); Date is used for Expires arithmetic and response ordering, both of which compare origin time with origin timeFR-FRS-4TestOriginClockSkewDoesNotStale

Multi-tenant and operational

IDThreatAnswerRefsTest
T-25Tenant A invalidates tenant B's groups (RFC 9875 §5)groups are scoped by origin (scheme, host, port); tenants on distinct hosts cannot touch each other. Tenants sharing one host share a group namespace; operators who host multiple parties on one host should set CacheGroups.IgnoreFR-PRG-6TestGroupsScopedByOrigin
T-26Unauthenticated purgePurge is a Go call; exposing it over HTTP is the adapter's job and must be authenticated (Caddy: admin API, which is local-only by default)08 §7manual review item
T-27Timing and Cache-Status side channel: learning whether a URL was recently requested (RFC 9111 §7.2)acceptable for a shared reverse-proxy cache of public content; NoCacheStatus: true disables the header; Weir never emits the key parameterFR-SRV-9TestCacheStatusNoKey