cursed_stale_policy¶
Required Knowledge¶
Info
Familiarity with Content Security Policy (CSP) concepts. OWASP - Testing for Content Security Policy
Content Security Policy (CSP) — the practical mental model¶
CSP is a browser-enforced ruleset sent by the server that restricts what a page is allowed to load/execute (scripts, styles, images, frames, network requests). It’s mainly used to reduce the impact of injection bugs like XSS.
- Overview: MDN CSP guide
- Header reference: MDN Content-Security-Policy header
- Spec: W3C CSP Level 3
CSP is “allowlist rules,” not a bug fix¶
- CSP doesn’t remove vulnerabilities; it limits what an attacker’s injected content can do (defense in depth).
The two pieces you read every time¶
Directives (what type of thing is being controlled)¶
Examples:
default-src(fallback policy)script-src(JavaScript execution rules)style-src,img-src,connect-src,frame-src,object-src, etc.
Source expressions (where it’s allowed to come from)¶
Common ones:
'self'= same originhttps://cdn.example.com= specific originsdata:/blob:= special schemes (often risky if used loosely)'unsafe-inline'= allows inline scripts/handlers (usually weak)'nonce-…'= allow inline scripts only if they include the right nonce'sha256-…'= allow inline scripts only if the content matches a hash
“Strict CSP”: nonces, hashes, and strict-dynamic¶
Modern “strict CSP” usually means:
- You trust scripts via a per-response nonce or a hash
- You avoid brittle domain allowlists for scripts
'strict-dynamic' is a common add-on in strict policies that changes how trust
is propagated from nonce/hash-trusted scripts.
Important gotcha (very challenge-relevant):
- A nonce-based CSP is only strong if the nonce is fresh per response/request.
Enforce vs Report-Only (great for debugging)¶
Content-Security-Policy= enforced (blocks violations)Content-Security-Policy-Report-Only= doesn’t block; it only reports what would’ve been blocked
Reporting (how defenders see violations)¶
- Modern reporting uses
report-to(paired with the Reporting API headers) - Legacy reporting uses
report-uri(deprecated but still seen)
Learn to “read” a CSP in 30 seconds (HTB-friendly)¶
- Find the CSP in DevTools → Network → the document request → Response Headers
-
Identify the script gate:
- Is there a
script-src? If not, doesdefault-srcapply? - Does it include
'unsafe-inline'? - Does it include a
'nonce-…'or'sha256-…'? - Is there
'strict-dynamic'?
- Is there a
-
When something fails, check the Console: browsers usually log CSP violations and name the directive that blocked it
Tiny examples (recognize patterns fast)¶
Weak-ish (inline allowed):
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com 'unsafe-inline';
Stricter (nonce-based “strict CSP” style):
Content-Security-Policy: default-src 'self'; script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none';
If you paste the exact CSP header from your HTB challenge, I’ll translate it into “what’s allowed / what’s blocked / what misconfigs to look for” without handing you a straight solution. (¬‿¬)
Solution¶
Here’s what that “solution” is really saying, step by step, in CSP terms — and why it works.
What the CSP nonce is supposed to do¶
- A CSP nonce is meant to be a one-time token that the server generates fresh per response.
- The policy says (conceptually): “Inline scripts are forbidden… except scripts that have this nonce.”
- So the browser only runs inline
<script>tags that match the nonce in thescript-srcdirective.
What went wrong: the nonce is static¶
- the nonce in
script-src 'nonce-…'does not change between page loads. - That breaks the security model: if the attacker can learn the nonce once (by loading the page), they can reuse it forever.
Why this matters:
- The nonce is basically acting like a password that's printed on every page load.
- If it never rotates, it’s not a one-time token; it’s just a constant allow-key.
How that enables script execution¶
If you can inject HTML (or otherwise get your content placed into the page), you can write:
<script nonce="STATIC_NONCE"> … </script>
Because the nonce matches the CSP header, the browser treats your injected script as “approved” and executes it even though inline scripts would normally be blocked.
So the bypass is:
- CSP tries to stop inline JS
- Nonce is intended to permit only trusted inline JS
- Static nonce lets anyone mint “trusted” scripts
Why the trigger XSS button matters¶
Challenges like this often include a bot (admin viewer) and a workflow like:
- You submit some content (comment/message/name/etc.)
- “Trigger XSS” makes the bot visit/render that content
So clicking “Trigger XSS” is basically:
- “Make the privileged bot load the page where my injected
<script nonce=...>exists”
If the injection is stored or reflected in a place the bot visits, the bot’s browser executes the script.
What /callback is doing in that description¶
That line means:
- The injected script is written to send a request to an endpoint like
/callback. - This is usually used as a “proof” channel:
- Either the bot hits
/callbackwith some data - Or
/callbackreturns something sensitive when called by the bot (because the bot has the right cookies/session)
- Either the bot hits
So the bot processes our script, makes a request to /callback implies the
payload caused a fetch/navigation/image load/etc. that reaches /callback under
the bot’s context.
How that leads to the flag page¶
Patterns:
/callbackreturns content only visible to an admin/bot session, so when the bot requests it, the response includes the flag.- Or
/callbackis part of a chain that causes the bot to load an internal/admin-only page and then reveal it somewhere the attacker can observe.
The xss payload used (from frontend/src/modules/violationHandler.js)¶
if (xssInputElement) {
xssInputElement.value = [
"<script>",
" fetch('/callback', {",
" method: 'POST',",
" headers: { 'Content-Type': 'application/json' },",
" body: JSON.stringify({ cookies: document.cookie })",
" });",
"</script>"
].join("\n");
window.codeMirrorEditors.xssEditor = initializeCodeMirrorEditor(xssInputElement, {
mode: 'javascript',
});
}
Reality check¶
Static/weak nonces + nonce-based CSP bypass chains¶
- HackerOne #2279346 - CSP bypass on PortSwigger.net (nonce-based CSP)
- HackerOne #2387458 - CSP bypass escalation discussion referencing #2279346
“CSP exists but is applied inconsistently” (coverage gaps)¶
- HackerOne #321 - CSP not consistently applied
- HackerOne #250729 - CSP not applied to error pages (HackerOne endpoints)
“CSP allowlists can be a trap” (trusted domains become the weakness)¶
- HackerOne #716677 - Domain takeover because a domain is whitelisted in CSP (GitLab)
- HackerOne #199779 - Google Analytics allowed in CSP abused as a bypass primitive
CSP reporting can leak info (report endpoints as a side-channel)¶
Older-but-useful CSP bypass thinking¶
- HackerOne #47472 - CSP bypass via click handler / event-ish behavior
- HackerOne #241192 - CSP policy bypass and related discussion