Course / 09 Web security / Lesson 15 of 28

Web cache deception

Estimated time: 16 min reading · 16 min activity

A cache stores a response so it can answer a later request without asking the application again. A cache key identifies which stored response to reuse. A shared cache serves multiple users, so it must not treat one user's private account response as a public file.

Web cache deception can occur when a cache and application interpret a path differently. Here the application treats an extra path segment as the account route, while the cache sees a .css suffix and stores the response as public content. The suffix is a claim about routing, not proof that the response is safe to share.

Before you begin

Estimated time
16 min reading, 16 min activity. Setup and optional practice may take longer.
Where you work
Read the original fictional requests, source fragments and state tables in a current desktop or mobile browser. The required activity needs no server, database, account or installed security tool. Do not send the printed requests to any service.
Without a lab
Use the complete printed evidence to trace the data or state on paper. Write the decision, the fact supporting it and one limit of the model, then compare with the activity rubric. This does not establish live service behavior. Reading works without JavaScript. Checking requires JavaScript and a secure browser context; saving also requires usable browser storage.

After this lesson you can:

  • Separate a private response from a shared cache.
  • Trace the three cache-flow steps and name whose body B receives.
  • Distinguish deception on this card from poisoning on the optional card.

01Example

Start with the intended behavior: a private account route bypasses shared storage. Then follow the different path used in the guided flow.

Caller A requests /account
Handler: account page
Cache decision: bypass private response
Result: no shared-cache entry created
Teaching model; no cache contacted.

02A suffix is not the handler

Follow three questions: which handler produces the response, whether the cache stores it, and who receives it next. Step 1 bypasses storage. Steps 2 and 3 show the route mismatch: a private response is stored under a public-looking path and reused across users. This is a fixed teaching sequence, not a captured cache exchange.

Align cache and application routing, and explicitly prevent shared storage of personalized responses. An unqualified Cache-Control: private response directive prohibits shared-cache storage; no-store asks caches not to store the response at all. These controls rely on compliant components and are not encryption. Cache poisoning is a different mechanism in which a harmful response is stored for later users.

Follow a private account body that a shared cache stores because the path looks like a static file.Model. A teaching model drawn by hand. It shows the idea, not real tool output.

Read the full explanation below.

Supplied private account response

Caller A requests /account; the handler is the account page.

Application handler

Requested routeOriginal: /accountNow: /accountfixed
Handler decisionOriginal: account pageNow: account pageprivate

Shared cache

Shared entryOriginal: absentNow: absentempty

Handler behavior and shared-storage policy matter; this worked normal route remains distinct from the later deceptive-path activity.

Caller A requests /account; the handler is the account page.

Follow the explanation

  1. Caller A requests /account; the handler is the account page.
  2. The private response bypasses the shared cache.
Read every state and value

1. Caller A requests /account; the handler is the account page.

Example
Supplied private account response
Items
Value: /account; Status: fixed · Value: account page; Status: Supplied private account response · Value: absent; Status: empty
Active Item
route · response
Event Label
Caller A requests /account; the handler is the account page.
Visible Limit
Handler behavior and shared-storage policy matter; this worked normal route remains distinct from the later deceptive-path activity.

2. The private response bypasses the shared cache.

Example
Supplied private account response
Items
Value: /account; Status: fixed · Value: bypass private response; Status: Supplied private account response · Value: absent; Status: no entry created
Active Item
entry
Event Label
The private response bypasses the shared cache.
Visible Limit
Handler behavior and shared-storage policy matter; this worked normal route remains distinct from the later deceptive-path activity.
Handler behavior and shared-storage policy matter; this worked normal route remains distinct from the later deceptive-path activity.

03Follow steps 2 and 3

Read the evidence before comparing your reasoning with the activity rubric. Do not contact a cache.

Shared cache model. No cache was contacted.

The account response is private to the caller.
Mark it private so a shared cache does not store it.
Also align routes: a path the cache treats as a public file must not be answered with the account page.

Deception on this card: the stored response belongs to one person and is later handed to another.
Poisoning on the optional card: the stored response is replaced, and later callers receive the replacement.

Download the cache rules

step,caller,path,app_handler,cache_decision,body_owner
1,A,/account,account page,bypass because private,A
2,A,/account/static.css,account page because the extra segment is ignored,store because the path ends in .css,A
3,B,/account/static.css,not called,serve the stored body,A

Download the cache flow

  1. Write the app handler and the cache decision for step 2.
  2. Write who receives the body in step 3, and who owns that body.
  3. Check: B is served A's account body. Step 1 did not store /account. The stored path is /account/static.css.
  4. Stop. Do not try this against a shared cache.

04Server-side case

Allow about fifteen minutes for this self-review. For each case, identify the untrusted value, the boundary crossed, the possible harm and a defense that addresses that mechanism. Cite the printed fact that supports the claim. Client-side and advanced topics remain part of the stage; later stages are available without a route lock.

Server-side case. Ungraded. Use the cards from this module. About fifteen minutes.

1. A lookup prints SELECT name FROM members WHERE id = ? and a separate tuple ('4',).
   Say whether the digits sit in the statement text or in the tuple.
2. Token BIRCH succeeds at 10:00 and is presented again at 10:05. The rule is single-use.
   Say whether the second presentation still meets the rule.
3. The joined label /lab/public/../archive/slip ends in which directory name?
4. A call is subprocess.run(["printf", "%s", "slip"], check=True, shell=False).
   Say whether a shell parser is started.
5. Order 19 already has one SAVE2 row. A later row applies SAVE2 again.
   Say whether the order rule allows that later row.
6. A public body still contains club-demo-key-not-real. Say what the public body should keep instead.
7. B reads shared row 9. The matrix says a shared row may be read by A or B.
   Say whether the owner check allows it.
8. badge.jpg bytes are 64 65 73 6B. Say what those bytes are: letters or the eight-byte image signature.
9. public_debug_page is true and the page prints session_store. Name the setting that closes that page.
10. In the ordered trace, B reads after A records a sale. Say whether B saw the updated stock.
11. The form holds a destination and the backend would download it. Name the fetcher.
12. Card R turns external resolution off. Say whether that card or pretty-print addresses the lookup.
13. username lane is a string. Say whether it meets a string schema.
14. The caller is editor and the method is GET for their own note. Read the matrix decision.
15. Step 3 of the cache flow serves A's account body to B. Say what crossed the user boundary.

Rubric: tuple; the second presentation does not meet the rule; archive; a shell parser is not started;
the later row is not allowed; the reference id; the shared read is allowed; letters;
public_debug_page; B saw the updated stock; the backend; Card R; it meets the schema;
the matrix decision for editor GET; A's account body was shown to B.

Download the server-side case

05Optional challenge

Contrast deception with poisoning using the two sentences on the rules card. The rubric is: deception hands one person's stored response to another, and poisoning replaces what later callers receive. About ten minutes.

06Fresh cases for the checks

Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.

Cache case A: new fixed teaching traceOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

Mira requests /profile/theme.css.
Application routing ignores the extra segment and generates Mira’s private profile body.
Shared cache routing treats .css as public and stores that body under this path.
Noel later requests the same path.
The cache answers from that stored entry without calling the application.
No cache or server is contacted; this is an authored model.

Cache case B: independent replacement scenarioOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

A stored public response is deliberately replaced with unwanted content. Later callers receive the replacement.
No private user body is involved in this separate scenario.

07Explain your evidence

Optional written self-check: explain one decision using the supplied case. No tool run is required.

Check your explanation: follow the handler, cache decision and body owner in order; identify the routing mismatch and cross-caller harm; explain private/no-store and their limits; distinguish response reuse from deliberate replacement.

08Common mistakes

If you say step 1 stored the account page, you missed the bypass. The store is step 2.

If you call this an outage, the card shows a body delivered, not a missing service.

If you treat a .css suffix as proof on a live cache, the card is a model. Real caches differ.

References

  • RFC 9111: HTTP Caching

    Sections 5.2.2.5 and 5.2.2.7 define no-store and private response directives. The former prohibits response storage; unqualified private prohibits shared-cache storage. These directives do not encrypt the response.

  • PortSwigger: Web cache deception

    Explains the failure mechanism and prevention. Use it for further reading after reasoning from this page's original model; a topic page is not evidence that a lab was launched.