これは何?
XSSがどのように発生するか改めて整理してみました。
GitHubにサンプルコード上げました。Dockerで簡単に起動できます。
XSSとは
XSS: Cross-Site Scriptingの略です。
ユーザ入力に対して動的にページ生成するアプリWebページにて悪意のある第三者が入力にJavaScriptを挿入することで,スクリプトを実行できてしまう脆弱性のことです。
- 反射型XSS(Reflected XSS)
- 格納型XSS(Stored XSS)
- DOM Based XSS
の3種類に分類されることが多いです。
反射型
レスポンスに含まれる文字列をそのまま返す場合に<script>
などがそのまま解釈されることで,JavaScriptが実行されます。
以下の例ではhttps://httbbin.org/anything という送ったリクエストをそのまま返してくれるページに<img src=x onerror=alert('hoge')>
という文字列を送信することでXSSが発火することが確認できます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fetch and Display Response</title>
</head>
<body>
<h1>Refrected XSS </h1>
<pre id="response-output">Loading...</pre>
<script>
// Fetchリクエストを送信
fetch("https://httpbin.org/anything", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// body: JSON.stringify({ key: "<s>xss</s>" }),
body: JSON.stringify({ key: "<img src=x onerror=alert('xss')>" }),
})
.then((response) => response.json()) // レスポンスをJSONとして解析
.then((data) => {
// レスポンスを画面に表示
const output = document.getElementById("response-output");
output.innerHTML = JSON.stringify(data, null, 4); // 整形して表示
})
.catch((error) => {
// エラーメッセージを画面に表示
const output = document.getElementById("response-output");
output.innerHTML = "Error";
});
</script>
</body>
</html>
格納型XSS(Stored XSS)
メカニズム的には反射型とほぼ同じなのでコードは割愛します。
ブログサイトなどで攻撃者が入力したスクリプトが含まれる文字列を、Webアプリケーション内部で永続的に保存し、レスポンスにそのコードが含まれるたびにXSSが発火します。
攻撃者が仕込んだスクリプトが不特定多数のユーザのブラウザで実行されるケースが多いため,被害の拡大という点では反射型よりも厄介だなと個人的には思います。
DOM型
ユーザのブラウザ上でJavaScriptがDOM操作を行い,画面を変化させている場合に攻撃者が仕込んだスクリプトが実行され,XSSが発火するパターンです。
サンプルコードはユーザが打ち込んだ文字列をそのまま表示するものです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vulnerable XSS Example</title>
<script>
/**
* XSSを試すためにフォームに入力された文字列をそのまま出力する
*/
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector("form");
form.addEventListener("submit", function(event) {
event.preventDefault(); // NOTE: formがsubmitされないようにすることで折返しの文字列がすぐに消えないようにする
// Formのinput要素から値を取得する
const input = document.getElementById("input").value;
// 折り返す
const output = document.getElementById("output");
output.innerHTML = "Hello, " + input + "!";
});
});
</script>
</head>
<body>
<h1>Vulnerable XSS Example</h1>
<form action="" method="GET">
<label for="input">Enter your name:</label>
<input type="text" id="input" name="name">
<button type="submit">Submit</button>
</form>
<div id="output">
</div>
</body>
</html>
<img src=x onerror=alert('hoge')>
を入力することでXSSが発火するのが確認できます。
XSSの対策
<
のような記号はサニタイジングすることが重要です。
本来は自前で関数を使ってサニタイジングすることはないですが,今回はサンプルとして特殊記号をサニタイジングする例を載せておきます。
これは先程のDOM Based XSSのサンプルにHTMLに使う記号をサニタイジングする処理を追加したものです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vulnerable XSS Example</title>
<script>
// 追加
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* XSSを試すためにフォームに入力された文字列をそのまま出力する
*/
document.addEventListener("DOMContentLoaded", function() {
const form = document.querySelector("form");
form.addEventListener("submit", function(event) {
event.preventDefault(); // NOTE: formがsubmitされないようにすることで折返しの文字列がすぐに消えないようにする
// Formのinput要素から値を取得する
const input = document.getElementById("input").value;
// 折り返す
const output = document.getElementById("output");
// 変更
output.innerHTML = "Hello, " + escapeHtml(input) + "!";
});
});
</script>
</head>
<body>
<h1>Vulnerable XSS Example</h1>
<form action="" method="GET">
<label for="input">Enter your name:</label>
<input type="text" id="input" name="name">
<button type="submit">Submit</button>
</form>
<div id="output">
</div>
</body>
</html>
これで文字列がきちんとサニタイジングされているため,XSSが発火しません。