Chain HTML injection in user profile bios with web cache deception via Nginx/Express path delimiter confusion to exfiltrate an admin API token from a cached dashboard page, then replay the token through a second cache deception to retrieve the flag from an admin-only secrets endpoint without an admin session.
The application is a Node.js (Express) portal behind an Nginx reverse proxy. Users can register, update profile bios, and submit URLs for an admin bot to review. The admin bot logs in via Puppeteer, visits the dashboard (where all user bios render), then navigates to the submitted URL.
Key observations from the source:
server.js:62-76) strips <script>, on* attributes, javascript:, and specific tags, but allows <style>, <link rel="stylesheet">, and arbitrary <div> attributes.dashboard.ejs:6) uses style-src 'self' 'unsafe-inline' * and img-src 'self' data: *, allowing external stylesheet loads and image fetches from any origin.nginx.conf:11-22) caches any response whose request URI matches \.(css|js|woff2|...)$ for 60 seconds. Cache ignores Set-Cookie headers and forwards session cookies to the backend.server.js:33-38) strips path parameters after semicolons: /dashboard;foo.css becomes /dashboard.dashboard.ejs:26) renders a secrets link with the admin API token in the href: <a id="secrets-link" href="/admin/secrets?token=TOKEN">.server.js:200-206) requires admin session AND matching API token to return the flag.Path delimiter confusion. Express treats semicolons as path parameter delimiters (via custom middleware), stripping everything after ; in each path segment. Nginx does not recognize ; as special and evaluates /dashboard;foo.css against the \.(css|js|...)$ location regex, matching it as a cacheable static asset.
When the admin bot navigates to /dashboard;foo.css:
*.css, enables caching, proxies to Express;foo.css, routes to /dashboard, renders the full dashboard HTML including the secrets link with the admin's API tokenAny subsequent request to the same URI (including unauthenticated requests) receives the cached dashboard page.
HTML injection via bio. The sanitizer allows <link rel="stylesheet"> tags. An attacker sets their bio to:
<link rel="stylesheet" href="/admin/secrets;flag.css?token=EXTRACTED_TOKEN">
When the admin visits the attacker's profile, the browser loads this "stylesheet" from the same origin. Nginx matches *.css, caches the response (the flag JSON), and the attacker replays the cached URL.
http://TARGET/dashboard;RAND.css.<a id="secrets-link" href="/admin/secrets?token=TOKEN">.<link rel="stylesheet" href="/admin/secrets;RAND.css?token=TOKEN">.<link> URL, sending the admin session cookie. Express serves the flag JSON. Nginx caches it./debug/sessions exposes session IDs but not session data. Without the session secret, forging a session cookie from a session ID is infeasible./redirect parameter looks like an open redirect but sanitizes the target URL, only allowing localhost and 127.0.0.1 origins for absolute URLs./metrics endpoint is ACL-restricted to 127.0.0.1 and 172.16.0.0/12 in Nginx, so external SSRF through it is blocked.