Là gì
Cache deception là ngược lại của cache poisoning: thay vì đầu độc một trang công khai, kẻ tấn công lừa cache lưu một trang RIÊNG TƯ của nạn nhân (trang tài khoản, dữ liệu cá nhân) rồi tự đọc nó. Cơ chế là cache và server bất đồng về "URL này là tài nguyên tĩnh hay động".
Vì sao bạn quan tâm
Điều phải nắm: cache deception khai thác một sự bất đồng về hậu tố URL. Cache thường quyết định "cache hay không" theo phần mở rộng — .css, .js, .jpg là tĩnh, cache được. Nhưng nhiều server bỏ qua phần thừa của URL và vẫn phục vụ trang động. Ghép hai điều đó lại:
- Kẻ tấn công lừa nạn nhân mở
https://app.example/account/profile.css. - Server bỏ qua
/profile.css, phục vụ trang/account/profilecủa nạn nhân (kèm dữ liệu cá nhân). - Cache thấy đuôi
.css, nghĩ đây là tĩnh, và LƯU response — kèm dữ liệu riêng của nạn nhân. - Kẻ tấn công mở cùng URL
.../profile.css, cache phục vụ bản đã lưu → đọc được dữ liệu nạn nhân.
Điểm khác biệt với cache poisoning: poisoning ghi một bản độc vào cache công khai; deception đọc một bản riêng tư ra khỏi cache. Cả hai là cùng gốc — cache và server bất đồng — nhưng hướng ngược nhau.
Và giống hai topic cache/smuggling khác: bản vá ở cấu hình cache, không ở code. Nó là một lỗi của việc cache quyết định "cache gì" theo một quy tắc (phần mở rộng) khác với quy tắc server dùng để quyết định "phục vụ gì".
Cơ chế hoạt động
Cơ chế là bất đồng về hậu tố URL giữa cache (quyết định theo phần mở rộng) và server (bỏ qua phần thừa).
sequenceDiagram autonumber actor A as Kẻ tấn công actor V as Nạn nhân (đã đăng nhập) participant C as Cache (cache theo .css) participant S as Server (bỏ qua /profile.css) A->>V: Lừa mở link /account/profile.css V->>C: GET /account/profile.css<br/>Cookie: session=<của nạn nhân> C->>S: chuyển tiếp (chưa có trong cache) S-->>C: 200, trang profile của NẠN NHÂN<br/>(server bỏ qua .css) Note over C: Cache thấy đuôi .css → nghĩ TĨNH → LƯU response<br/>(kèm dữ liệu riêng của nạn nhân). A->>C: GET /account/profile.css (KHÔNG cookie) C-->>A: bản đã lưu — dữ liệu của NẠN NHÂNĐiểm cốt lõi: cache lưu một trang cần đăng nhập vì nó tưởng đó là tĩnh. Kẻ tấn công không cần phiên nạn nhân để đọc — cache phục vụ bản đã lưu cho bất kỳ ai hỏi cùng URL, kể cả không cookie.
Bảng điều kiện — cả hai phải đúng thì mới khai thác được:
| Điều kiện | Chi tiết |
|---|---|
| Server bỏ qua phần thừa của URL | /account/profile.css phục vụ như /account/profile |
| Cache quyết định theo phần mở rộng | .css/.js/.jpg → cache, bỏ qua Cache-Control của trang |
| Trang có dữ liệu riêng tư | Trang tài khoản, dữ liệu cá nhân — thứ đáng đọc trộm |
Các biến thể ở phần "server bỏ qua phần thừa": không chỉ .css. /account/profile/xyz (path thừa), /account/profile;.css (path parameter), /account/profile%00.css (null byte), /account/profile.css?x=1 — mỗi cách là một sự bất đồng khác giữa cách server parse URL và cách cache quyết định. Đây là lý do blocklist phần mở rộng không đủ (giống các topic injection).
Mô tả sơ đồ: Sơ đồ tuần tự một cuộc cache deception. Kẻ tấn công lừa nạn nhân đang đăng nhập mở link /account/profile.css. Trình duyệt nạn nhân gửi request đó kèm cookie phiên của nạn nhân tới cache; cache chưa có nên chuyển tiếp tới server. Server bỏ qua đuôi .css và phục vụ trang profile của nạn nhân kèm dữ liệu cá nhân. Cache thấy đuôi .css nên nghĩ đây là tài nguyên tĩnh và lưu response đó, kèm dữ liệu riêng của nạn nhân. Sau đó kẻ tấn công mở cùng URL profile.css mà không cần cookie, và cache trả lại bản đã lưu — dữ liệu của nạn nhân.
Ví dụ cụ thể
Một trang tài khoản GET /account/profile cần đăng nhập, sau một CDN cache theo phần mở rộng.
# Kẻ tấn công lừa nạn nhân mở URL này (qua email, link chat). Nạn nhân đã đăng nhập.GET /account/profile.css HTTP/1.1Host: app.exampleCookie: session=<phiên của nạn nhân># Server BỎ QUA ".css" và phục vụ trang profile động của nạn nhân:HTTP/1.1 200 OKContent-Type: text/htmlCache-Control: private, no-store ← trang NÓI đừng cache <h1>Alice Nguyen</h1><p>Email: alice@acme.com · Thẻ: **** 4242</p># Nhưng CACHE quyết định theo ĐUÔI .css và BỎ QUA Cache-Control: private của trang.# Nó lưu response — kèm dữ liệu của Alice — dưới key /account/profile.css.# Kẻ tấn công mở cùng URL, KHÔNG cần cookie của Alice:GET /account/profile.css HTTP/1.1Host: app.example(không cookie) HTTP/1.1 200 OK (từ cache)<h1>Alice Nguyen</h1><p>Email: alice@acme.com · Thẻ: **** 4242</p> ← dữ liệu của Alice# Phát hiện: mở một trang riêng tư với đuôi tĩnh giả, rồi mở lại KHÔNG cookie.curl -s https://app.example/account/profile.css -b "session=$MY_SESSION" > /dev/nullcurl -s https://app.example/account/profile.css | grep -q "alice@acme.com" \ && echo "CACHE DECEPTION: dữ liệu riêng đọc được không cần cookie"# Sau khi vá: cache tôn trọng Cache-Control: private, VÀ server từ chối /profile.css# (không bỏ qua phần thừa), VÀ cache chỉ lưu theo Content-Type thật, không theo đuôi.# Cấu hình CDN. Cache deception sống Ở ĐÂY, không trong code app.cache: rules: # ❌ Cache theo PHẦN MỞ RỘNG URL. Một trang tài khoản mở với đuôi .css được cache # NHƯ MỘT tài nguyên tĩnh — kèm dữ liệu riêng của người dùng trong response. # Đây là điều kiện 2 của khối 3. - match: "\\.(css|js|jpg|png|woff2|svg)$" action: cache ttl: 86400 # ❌ BỎ QUA Cache-Control của origin. App đặt "private, no-store" cho trang tài # khoản, nhưng CDN ghi đè nó và cache theo đuôi ở trên. Nên một app ĐÚNG vẫn bị # deception vì cấu hình cache sai. respect_origin_cache_control: false # Và ở server: router khớp "bắt đầu bằng", nên /account/profile.css khớp route# /account/profile và server phục vụ trang động — điều kiện 1 của khối 3.# routes:# - path_prefix: /account/profile # ❌ prefix, nên .css đi kèm cũng khớpChuyện đã xảy ra
Omer Gil — "Web Cache Deception Attack" (Black Hat 2017). Bài trình bày định nghĩa lớp lỗ hổng này và cho thấy nó hoạt động trên các dịch vụ lớn (bao gồm một demo nổi tiếng trên PayPal thời điểm đó): mở một URL trang tài khoản với đuôi .css, và cache lưu trang có dữ liệu cá nhân. Đây là nguồn của bảng điều kiện ở khối 3.
Các sự cố CDN theo mặc định cache-by-extension (nhiều báo cáo). Nhiều CDN mặc định cache mọi thứ có đuôi tĩnh, và mặc định đó ghi đè Cache-Control: private của app. Nên một app đúng (đặt no-store cho trang tài khoản) vẫn bị deception nếu CDN cấu hình cache-by-extension. Minh hoạ luận điểm khối 2: bản vá ở cấu hình cache, không ở code app.
Nghiên cứu mở rộng của Kettle/Gil về các biến thể URL (2020). Cho thấy .css chỉ là một cách; path thừa, path parameter (;), null byte, và các cách parse URL khác giữa cache và server đều mở ra deception. Đây là lý do khối 6 nói bản vá phải là "cache và server ĐỒNG Ý về URL này là gì", không phải "chặn đuôi .css".
Cách phòng chống
Cache theo `Content-Type` và `Cache-Control` của app, KHÔNG theo phần mở rộng URL
bắt buộcNguyên nhân gốc là cache quyết định "cache gì" theo phần mở rộng, ghi đè điều app nói. Bản vá là làm cache TÔN TRỌNG điều app nói.
- Cache tôn trọng
Cache-Controlcủa response. Nếu app đặtprivate, no-store, cache KHÔNG được lưu — bất kể đuôi URL. Đây là dòng quan trọng nhất, và nó là một cấu hình CDN: tắt "cache theo phần mở rộng" và bật "tôn trọng origin Cache-Control". - Chỉ cache theo
Content-Typethật. Một responseContent-Type: text/htmlKHÔNG được cache như tĩnh dù URL kết thúc.css. Cache dựa vào những gì server TRẢ VỀ, không vào những gì URL GỢI Ý. - App đặt
Cache-Control: no-storecho mọi trang phụ thuộc phiên/người dùng — đây là nửa ở phía app, và nó phải có kể cả khi tin cache tôn trọng nó. Phòng thủ chiều sâu.
Điểm quan trọng: đây là quyết định cấu hình cache, và nó phải rõ ràng. Nhiều CDN mặc định cache-by-extension vì nó nhanh; bạn phải tắt nó tường minh cho các path có nội dung động.
cache: # Mặc-định-ĐÓNG: chỉ cache các PREFIX tĩnh đã biết, mọi thứ khác không cache. Đây là # biện pháp mạnh nhất (lớp 2) — một trang động mới thêm KHÔNG bị cache do sơ ý, giống # nguyên tắc [Authorize] toàn cục ở topic access-control. default: no-cache rules: - match_prefix: ["/_next/static/", "/assets/"] # tài nguyên tĩnh có hash tên file action: cache ttl: 31536000 # tĩnh bất biến → cache lâu là an toàn # TÔN TRỌNG Cache-Control của origin. Nếu app nói private/no-store, cache KHÔNG lưu — # bất kể đuôi URL. Đây là dòng đóng deception ở phía cache. respect_origin_cache_control: true # Cache theo CONTENT-TYPE thật, không theo đuôi URL. Một response text/html KHÔNG # được cache như tĩnh dù URL kết thúc .css — cache dựa vào những gì server TRẢ VỀ. cache_by: content_type cacheable_content_types: ["text/css", "application/javascript", "image/*", "font/*"] server_routing: | # Nửa phía server (lớp 1b): router khớp path CHÍNH XÁC, không "bắt đầu bằng". # /account/profile.css KHÔNG khớp route /account/profile → trả 404, không phục vụ # trang động. Cache và server ĐỒNG Ý về "URL này là gì". app.MapGet("/account/profile", ...); // khớp đúng "/account/profile" // /account/profile.css, /account/profile/x, /account/profile;.css → 404 app_headers: | # Và app đặt no-store cho MỌI trang phụ thuộc phiên — nửa phía app. Phòng thủ # chiều sâu: kể cả khi tin cache tôn trọng nó, app vẫn nói ra. [ResponseCache(NoStore = true)] // trên controller của trang tài khoản // → Cache-Control: no-store, và cache (đã cấu hình respect) sẽ nghe.Cache và server phải ĐỒNG Ý về "URL này là gì"
bắt buộcDeception là một sự bất đồng, nên bản vá thứ hai là loại bỏ khả năng bất đồng — giống topic request-smuggling.
- Server không bỏ qua phần thừa của URL.
/account/profile.cssphải là 404, không phải phục vụ/account/profile. Một router khớp CHÍNH XÁC đường dẫn (không "bắt đầu bằng") thì/profile.csskhông khớp route/profile. Đây là nửa ở phía server. - Chuẩn hoá URL nhất quán giữa cache và server. Nếu server coi
/profile;.css,/profile%00.css,/profile/xlà/profilenhưng cache coi chúng là tĩnh, có chỗ cho deception. Cả hai phải parse URL giống nhau — đây là lý do blocklist đuôi.csskhông đủ (bảng biến thể ở khối 3).
Nguyên tắc chung: cache và server là hai bộ parse URL khác nhau, và mọi chỗ chúng bất đồng là một lỗ hổng — cùng bài học với request-smuggling (cache/backend bất đồng về ranh giới request) và web-cache-poisoning (bất đồng về cache key).
Allowlist đường dẫn được cache, không blocklist
Biện pháp mạnh nhất về cấu hình là đảo mặc định: cache KHÔNG cache gì trừ khi được liệt kê rõ.
- Chỉ cache các đường dẫn/prefix tĩnh đã biết (
/static/,/assets/,/_next/static/), và mọi thứ khác mặc định không cache. Đây là mặc-định-đóng, giống nguyên tắc[Authorize]toàn cục ở topic access-control: một trang động mới thêm vào KHÔNG tự nhiên bị cache. - Ngược lại — blocklist các đường dẫn động — là mặc-định-MỞ, và một trang tài khoản mới quên thêm vào blocklist sẽ bị cache. Cùng lý do blocklist thua ở các topic injection.
Với kiến trúc hiện đại (Next.js, static assets có hash tên file), allowlist rất tự nhiên: tài nguyên tĩnh nằm dưới một prefix biết trước, nên "chỉ cache prefix đó" vừa an toàn vừa đơn giản. SecLab dùng đúng cách này — chỉ _next/static và /assets được cache.
Phát hiện: dữ liệu riêng tư trong response được cache
Deception khó phát hiện vì nạn nhân không thấy gì bất thường — kẻ tấn công đọc dữ liệu ở một request riêng. Hai biện pháp:
- Synthetic canary: một job đăng nhập bằng một tài khoản test, mở các trang riêng tư với các đuôi giả (
.css,.js, path thừa), rồi mở lại KHÔNG cookie và kiểm response có chứa dữ liệu của tài khoản test không. Nếu có → cache đang lưu trang riêng tư (kỹ thuật ở khối 4, tự động). - Log request tới đường dẫn động có đuôi tĩnh (
/account/*.css,/api/*.js) — client hợp lệ không tạo ra chúng, nên chúng là chữ ký của một lần dò deception. - Kiểm response được cache có header
Set-CookiehayCache-Control: private— một response cá nhân hoá lọt vào cache là một cảnh báo đỏ; scanner ở tầng CDN có thể bắt được.
Đây là lớp 3 vì cấu hình ở lớp 1 đã chặn; giám sát cho biết nếu một cấu hình sai lọt qua.
Kiểm chứng đã vá
1. Kỹ thuật canary — phép kiểm cốt lõi, và nó là hai bước (giống cache poisoning nhưng đọc, không ghi):
B=https://staging.example.com# Bước 1: đăng nhập bằng tài khoản test, mở trang riêng tư với đuôi tĩnh giả.curl -s "$B/account/profile.css" -b "session=$TEST_SESSION" | grep -q "$TEST_EMAIL" \ && echo "server bỏ qua .css và phục vụ trang riêng"# Bước 2: mở CÙNG URL không cookie — dữ liệu test KHÔNG được xuất hiện.sleep 1curl -s "$B/account/profile.css" | grep -q "$TEST_EMAIL" \ && { echo "CACHE DECEPTION: dữ liệu riêng đọc được không cần đăng nhập"; exit 1; }exit 02. Thử nhiều biến thể URL — bảng ở khối 3: không chỉ .css:
for u in /account/profile.css /account/profile.js /account/profile/x \ "/account/profile;.css" "/account/profile%00.css"; do curl -s "$B$u" -b "session=$TEST_SESSION" > /dev/null curl -s "$B$u" | grep -q "$TEST_EMAIL" && echo "DECEPTION qua $u"done3. Kiểm server TỪ CHỐI đường dẫn có đuôi thừa (nửa phía server ở lớp 1b):
# /account/profile.css phải là 404, không phục vụ /account/profile.curl -s -o /dev/null -w '%{http_code}\n' "$B/account/profile.css" -b "session=$TEST_SESSION"# Mong đợi 404. Nếu 200 → server bỏ qua phần thừa, điều kiện 1 của khối 3 đúng.4. Kiểm cache tôn trọng Cache-Control: private — đây là kiểm cấu hình CDN:
# Response của trang riêng phải có Cache-Control private/no-store...curl -sI "$B/account/profile" -b "session=$TEST_SESSION" | grep -i cache-control# ...VÀ CDN phải tôn trọng nó (không cache-by-extension). Kiểm cấu hình CDN trực tiếp.5. Kiểm allowlist cache (lớp 2) — xác nhận cấu hình CDN chỉ cache các prefix tĩnh đã biết (/_next/static, /assets), mọi thứ khác mặc định không cache. Đây là kiểm cấu hình, không phải test code.
### Web cache deception — kỹ thuật canary (httpyac/REST Client hoặc script)###### Ngược với cache poisoning: ở đây ta kiểm cache có LƯU một trang RIÊNG TƯ và phục### vụ nó cho người KHÔNG có cookie hay không. Hai bước, và bước 2 mới chứng minh lỗ hổng. ### Bước 1 · đăng nhập (tài khoản TEST) và mở trang riêng với đuôi tĩnh giảGET https://staging.example.com/account/profile.css HTTP/1.1Cookie: session={{TEST_SESSION}} ### Kỳ vọng SAU KHI VÁ: 404 (server không phục vụ /profile.css như /profile).### Nếu 200 và chứa email của tài khoản test → server bỏ qua .css (điều kiện 1, khối 3). ### Bước 2 · mở CÙNG URL, KHÔNG cookie — đây là request của "kẻ tấn công"GET https://staging.example.com/account/profile.css HTTP/1.1 ### Kỳ vọng SAU KHI VÁ: KHÔNG chứa email của tài khoản test (cache không lưu trang riêng).### Nếu CHỨA → cache đã lưu trang riêng của tài khoản test và phục vụ nó cho một request### không đăng nhập. Đây là cache deception đã xảy ra — dữ liệu riêng đọc được không cần cookie. ### Bước 3 · thử các biến thể URL (bảng khối 3: không chỉ .css)GET https://staging.example.com/account/profile/x HTTP/1.1Cookie: session={{TEST_SESSION}} GET https://staging.example.com/account/profile;.css HTTP/1.1Cookie: session={{TEST_SESSION}} ### Bước 4 · kiểm response trang riêng có Cache-Control đúngGET https://staging.example.com/account/profile HTTP/1.1Cookie: session={{TEST_SESSION}} ### Kỳ vọng: header Cache-Control chứa "no-store" hoặc "private". Nhưng lưu ý: header### đúng ở app KHÔNG đủ nếu CDN cache-by-extension ghi đè nó — bản vá thật ở cấu hình### cache (xem tab yaml), và các request này là cách CHỨNG MINH nó đúng.Sai lầm thường gặp
| "Bản vá" | Vì sao không đúng |
|---|---|
Đặt Cache-Control: private ở app rồi coi là xong | Cache-by-extension GHI ĐÈ nó ở nhiều CDN. Cache phải được cấu hình tôn trọng Cache-Control |
Blocklist đuôi .css/.js trên trang động | Còn path thừa, ;, null byte, và các cách parse URL khác (bảng khối 3). Allowlist prefix tĩnh |
| Cache theo phần mở rộng URL | Đây là nguyên nhân. Cache theo Content-Type thật và Cache-Control của app |
Server phục vụ /profile.css như /profile | Điều kiện 1 của khối 3. Router phải khớp path CHÍNH XÁC → /profile.css là 404 |
| Chỉ vá ở app | Bản vá chính ở CẤU HÌNH CACHE. App đặt no-store là nửa phía app, cache tôn trọng là nửa phía cache |
| Coi nó là cache poisoning | Ngược chiều: poisoning GHI bản độc vào cache công khai; deception ĐỌC bản riêng ra khỏi cache |
Sai lầm về mô hình, và nó là sai lầm chính: nghĩ Cache-Control: private ở app là đủ. Nhiều CDN cache theo phần mở rộng và ghi đè header đó — nên bản vá phải ở cấu hình cache: tôn trọng Cache-Control, cache theo Content-Type, và allowlist prefix tĩnh.
Sai lầm về vị trí: tìm bản vá trong code. Như request-smuggling và web-cache-poisoning, đây là một lỗi của sự BẤT ĐỒNG giữa hai thành phần hạ tầng (cache và server) về "URL này là gì", nên bản vá sống ở nơi giải quyết bất đồng đó — cấu hình cache và định tuyến server, không phải một handler.
Bình luận
Bình luận cần tài khoản đã hoàn thành ít nhất một bài học. Điều kiện đó là thứ giữ cho luồng thảo luận này còn đáng đọc: mỗi ý kiến gắn với một người có thể bị hỏi lại, và reputation tích luỹ theo thời gian.
Bạn vẫn đọc được toàn bộ bình luận dưới đây mà không cần tài khoản. Đăng nhập xong bạn sẽ quay lại đúng chỗ này, không phải đầu trang.
Đang tải bình luận…