Table of Contents
Introduction
Path Mapping Discrepancies
Delimiter Discrepancies
Delimiter Decoding Discrepancies
Static Directory Cache Rules
Normalization Discrepancies
File Name Cache Rules
Browser-Specific Considerations
Single-Page Applications (SPAs)
REST APIs
GraphQL APIs
gRPC APIs
Prevention Strategies
Introduction Web Cache Deception is an attack where an attacker tricks a cache into storing sensitive dynamic content under a static URL, allowing unauthorized access to that content.
Basic Attack Flow
Attacker identifies a cacheable endpoint with sensitive data
Attacker crafts a URL that appears to request a static resource
The cache treats it as a static request and stores the response
The origin server ignores the static-looking part and returns sensitive data
The sensitive data gets cached under a static URL
Attacker can now access the sensitive data via the static URL
Path Mapping Discrepancies Vulnerable Server Code (Express.js) 1 2 3 4 5 6 7 8 9 10 11 const express = require ('express' );const app = express ();app.get ('/profile/:id' , (req, res ) => { const userId = req.params .id ; res.json ({ userId, data : "Sensitive profile data" }); }); app.listen (3000 );
Exploitation Steps
Identify a sensitive endpoint (e.g., /profile/123)
Try adding a static extension: /profile/123.js
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Manually add various extensions to sensitive paths: .js, .css, .ico, .png
Use Burp Suite to send requests and check for caching behavior
Verify if the response contains sensitive data
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const express = require ('express' );const app = express ();app.get ('/profile/:id' , (req, res ) => { if (req.path !== `/profile/${req.params.id} ` ) { return res.status (404 ).send ('Not found' ); } const userId = req.params .id ; res.json ({ userId, data : "Sensitive profile data" }); }); app.listen (3000 );
Delimiter Discrepancies Vulnerable Server Code (Java Spring) 1 2 3 4 5 6 7 8 9 10 @RestController public class ProfileController { @GetMapping("/profile") public String getProfile () { return "User profile data" ; } }
Exploitation Steps
Identify a sensitive endpoint (e.g., /profile)
Try adding a delimiter and static extension: /profile;user.css
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various delimiters: ;, ?, #, :
Combine with static extensions: .js, .css, .ico
Check for caching behavior
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class ProfileController { @GetMapping("/profile") public String getProfile (HttpServletRequest request) { String path = request.getRequestURI(); if (path.contains(";" )) { throw new IllegalArgumentException ("Invalid path" ); } return "User profile data" ; } }
Delimiter Decoding Discrepancies Vulnerable Server Code (Node.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const express = require ('express' );const app = express ();app.use ((req, res, next ) => { req.url = decodeURIComponent (req.url ); next (); }); app.get ('/account' , (req, res ) => { res.json ({ data : "Sensitive account information" }); }); app.listen (3000 );
Exploitation Steps
Identify a sensitive endpoint (e.g., /account)
Try URL-encoding a delimiter and adding a static extension: /account%3f.css
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test URL-encoded delimiters: %3f (?), %2f (/), %3b (;), %23 (#)
Combine with static extensions: .js, .css, .ico
Check for caching behavior
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const express = require ('express' );const app = express ();app.use ((req, res, next ) => { const originalUrl = req.url ; const decodedUrl = decodeURIComponent (originalUrl); if (originalUrl !== decodedUrl && !isValidUrl (decodedUrl)) { return res.status (400 ).send ('Invalid URL' ); } req.url = decodedUrl; next (); }); function isValidUrl (url ) { return !url.includes ('?' ) && !url.includes ('#' ); } app.get ('/account' , (req, res ) => { res.json ({ data : "Sensitive account information" }); }); app.listen (3000 );
Static Directory Cache Rules Vulnerable Server Code (Nginx) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server { listen 80 ; server_name example.com; location /static/ { expires 1d ; add_header Cache-Control "public" ; try_files $uri $uri / @backend ; } location @backend { proxy_pass http://backend; } }
Exploitation Steps
Identify a sensitive endpoint (e.g., /account)
Try path traversal to a static directory: /static/../account
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various static directories: /static/, /assets/, /resources/, /public/
Use path traversal: ../, ../../, etc.
Check for caching behavior
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server { listen 80 ; server_name example.com; location / { merge_slashes on ; if ($uri ~* "\.\.") { return 400 ; } try_files $uri $uri / @backend ; } location @backend { proxy_pass http://backend; } }
Normalization Discrepancies Vulnerable Server Code (Apache with mod_rewrite) 1 2 3 4 5 6 7 8 9 10 11 RewriteEngine On RewriteCond %{REQUEST_URI} ^(.*)%2 f(.*)$RewriteRule ^ /%1 /%2 [L,NE] RewriteCond %{REQUEST_URI} ^(.*)/\.\./(.*)$RewriteRule ^ /%1 /%2 [L,NE]
Exploitation Steps
Identify a sensitive endpoint (e.g., /profile)
Try encoded path traversal to a static directory: /static/..%2fprofile
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test encoded path traversal: ..%2f, ..%2F, %2e%2e%2f
Combine with static directories: /static/, /assets/, etc.
Check for caching behavior
Fix 1 2 3 4 5 RewriteEngine On RewriteCond %{THE_REQUEST} %2 f [NC] RewriteRule ^ - [F]
File Name Cache Rules Vulnerable Server Code (Nginx) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server { listen 80 ; server_name example.com; location = /robots.txt { expires 7d ; add_header Cache-Control "public" ; try_files $uri @backend ; } location @backend { proxy_pass http://backend; } }
Exploitation Steps
Identify a sensitive endpoint (e.g., /account)
Try appending a cached filename: /account/robots.txt
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various cached filenames: robots.txt, favicon.ico, index.html
Check for caching behavior
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server { listen 80 ; server_name example.com; location = /robots.txt { if (!-f $request_filename ) { return 404 ; } expires 7d ; add_header Cache-Control "public" ; } location @backend { proxy_pass http://backend; } }
Browser-Specific Considerations URL Normalization in Browsers Before sending a request, browsers perform several normalization steps:
Scheme and Host Case Normalization : Converting scheme and host to lowercase
Path Normalization : Resolving dot-segments (. and ..)
Percent-Encoding Normalization : Decoding unreserved characters
Fragment Removal : Removing the fragment part (# and everything after it)
Exploitation Steps
Craft a URL that will be normalized differently by the browser and the server
For example, use encoded characters that the browser decodes but the server doesn’t
Check if the response is cached
Testing Methods
Test with different browsers (Chrome, Firefox, Safari, Edge)
Use browser developer tools to inspect the actual request being sent
Compare with what the server receives
Fix Implement consistent URL normalization on the server side to match browser behavior.
Single-Page Applications (SPAs) Vulnerable Server Code (Express.js) 1 2 3 4 5 6 7 8 9 10 const express = require ('express' );const path = require ('path' );const app = express ();app.get ('/app/*' , (req, res ) => { res.sendFile (path.resolve (__dirname, 'build/index.html' )); }); app.listen (3000 );
Exploitation Steps
Identify a sensitive route in the SPA (e.g., /app/profile)
Try adding a static extension: /app/profile.js
If the server returns the SPA with sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various static extensions: .js, .css, .ico
Check for caching behavior
Verify if the response contains sensitive data
Fix 1 2 3 4 5 6 7 8 9 10 11 12 const express = require ('express' );const path = require ('path' );const app = express ();app.get ('/app/*' , (req, res ) => { res.setHeader ('Cache-Control' , 'no-store' ); res.sendFile (path.resolve (__dirname, 'build/index.html' )); }); app.listen (3000 );
REST APIs Vulnerable Server Code (Express.js) 1 2 3 4 5 6 7 8 9 10 11 const express = require ('express' );const app = express ();app.get ('/api/users/:id' , (req, res ) => { const userId = req.params .id ; res.json ({ userId, data : "Sensitive user data" }); }); app.listen (3000 );
Exploitation Steps
Identify a sensitive API endpoint (e.g., /api/users/123)
Try adding a static extension: /api/users/123.js
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various static extensions: .js, .css, .ico
Check for caching behavior
Verify if the response contains sensitive data
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const express = require ('express' );const app = express ();app.get ('/api/users/:id' , (req, res ) => { if (req.path !== `/api/users/${req.params.id} ` ) { return res.status (404 ).send ('Not found' ); } const userId = req.params .id ; res.json ({ userId, data : "Sensitive user data" }); }); app.listen (3000 );
GraphQL APIs Vulnerable Server Code (Apollo Server) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const { ApolloServer , gql } = require ('apollo-server' );const typeDefs = gql` type User { id : ID! name : String! email : String! } type Query { user( id : ID! ) : User } ` ;const resolvers = { Query : { user : (parent, { id }, context, info ) => { return getUserById (id); } } }; const server = new ApolloServer ({ typeDefs, resolvers }); server.listen ().then (({ url } ) => { console .log (`Server ready at ${url} ` ); });
Exploitation Steps
Identify a sensitive GraphQL query (e.g., user(id: "123"))
Try adding a static extension to the endpoint: /graphql.js
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various static extensions: .js, .css, .ico
Check for caching behavior
Verify if the response contains sensitive data
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const { ApolloServer , gql } = require ('apollo-server' );const typeDefs = gql` type User { id : ID! name : String! email : String! } type Query { user( id : ID! ) : User } ` ;const resolvers = { Query : { user : (parent, { id }, context, info ) => { return getUserById (id); } } }; const server = new ApolloServer ({ typeDefs, resolvers, plugins : [ { requestDidStart ( ) { return { didResolveOperation (requestContext ) { const operation = requestContext.request .operationName ; if (operation === 'GetUser' ) { requestContext.response .http .headers .set ('cache-control' , 'no-store' ); } } }; } } ] }); server.listen ().then (({ url } ) => { console .log (`Server ready at ${url} ` ); });
gRPC APIs Vulnerable Server Code (Node.js gRPC) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const grpc = require ('@grpc/grpc-js' );const protoLoader = require ('@grpc/proto-loader' );const PROTO_PATH = './user.proto' ;const packageDefinition = protoLoader.loadSync (PROTO_PATH );const userProto = grpc.loadPackageDefinition (packageDefinition).user ;function getUser (call, callback ) { const userId = call.request .id ; callback (null , { id : userId, name : "John Doe" , email : "john@example.com" }); } const server = new grpc.Server ();server.addService (userProto.UserService .service , { getUser : getUser }); server.bindAsync ('0.0.0.0:50051' , grpc.ServerCredentials .createInsecure (), () => { server.start (); });
Exploitation Steps
Identify a sensitive gRPC method (e.g., getUser)
Try adding a static extension to the endpoint: /grpc.js
If the server returns the same sensitive data, proceed
Check if the response is cached (look for X-Cache: MISS on first request, HIT on second)
If cached, the sensitive data is now accessible via the static URL
Testing Methods
Test various static extensions: .js, .css, .ico
Check for caching behavior
Verify if the response contains sensitive data
Fix 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const grpc = require ('@grpc/grpc-js' );const protoLoader = require ('@grpc/proto-loader' );const PROTO_PATH = './user.proto' ;const packageDefinition = protoLoader.loadSync (PROTO_PATH );const userProto = grpc.loadPackageDefinition (packageDefinition).user ;function getUser (call, callback ) { const userId = call.request .id ; if (!userId || typeof userId !== 'string' ) { callback ({ code : grpc.status .INVALID_ARGUMENT , message : 'Invalid user ID' }); return ; } callback (null , { id : userId, name : "John Doe" , email : "john@example.com" }); } const server = new grpc.Server ();server.addService (userProto.UserService .service , { getUser : getUser }); server.bindAsync ('0.0.0.0:50051' , grpc.ServerCredentials .createInsecure (), () => { server.start (); });
Prevention Strategies General Prevention Techniques
Consistent URL Normalization
Implement consistent URL normalization across all components
Ensure the cache and origin server handle URLs in the same way
Proper Cache Headers
Use Cache-Control: no-store for sensitive content
Use Cache-Control: private for content that should only be cached by browsers
Input Validation
Validate all input, including URL paths
Reject suspicious characters and patterns
Cache Key Normalization
Normalize cache keys to ensure consistent behavior
Include relevant headers in the cache key
Testing and Monitoring
Regularly test for cache deception vulnerabilities
Monitor for unusual cache behavior
Technology-Specific Prevention For Web Applications
Use strict routing that doesn’t ignore extra path segments
Implement proper authentication and authorization
Set appropriate cache headers for different types of content
For APIs
Validate all input parameters
Use API gateways with proper caching configurations
Implement rate limiting to prevent abuse
For SPAs
Set appropriate cache headers for the SPA shell
Implement proper authentication checks
Use service workers with caution
For GraphQL
Normalize queries before generating cache keys
Implement proper authentication and authorization
Use persistent queries to prevent injection
For gRPC
Validate all request parameters
Implement proper authentication and authorization
Use interceptors to add security headers
CDN-Specific Prevention
Cloudflare
Enable “Cache Deception Armor”
Use Cloudflare Workers to normalize URLs
Akamai
Configure proper cache rules
Use “Path Normalization” feature
AWS CloudFront
Configure proper cache behaviors
Use Lambda@Edge to normalize URLs
Fastly
Configure proper cache rules
Use VCL to normalize URLs
By implementing these prevention strategies, you can significantly reduce the risk of web cache deception attacks.