こんにちは!Webアプリケーションの開発や、会社のITインフラの管理を任されるようになると、「セキュリティ」という言葉を避けて通れなくなりますよね。
「なんだか難しそうな専門用語ばかりで、何から手をつけたらいいか分からない…」
そんな風に悩んでいませんか?大丈夫です。一歩ずつ、身近な例えから紐解いていけば必ず理解できるようになりますよ。
今回は、数あるサイバー攻撃の中でも、ブラウザの裏側でこっそり悪さをする「DOMベースXSS(クロスサイトスクリプティング)」という脆弱性について、一緒に優しく学んでいきましょう!
—
1. 家の鍵に例える「DOMベースXSS」の仕組み
セキュリティの仕組みを考えるとき、よく「家と泥棒」に例えられます。今回学ぶDOMベースXSSも、まさにこの例えがぴったりなんです。
サーバー型(従来のXSS)とDOMベースの違い
これまでのXSSは、悪意あるデータがいったんサーバー(本棚)に保管され、それが他の人(お客さん)に配られるという形でした。これは例えるなら、「留守番の人が、知らない怪しい手紙を郵便受けから受け取って家の中に持ち込んでしまう」ような状態です。
一方で、今回の主役であるDOMベースXSSは、サーバーを一切経由しません。全部、あなたの手元のブラウザ(リビングルーム)の中だけで完結します。
玄関の鍵が開けっ放し!?
DOMベースXSSを「家」で例えてみましょう。
あなたの家のリビングには、外の様子を映し出す大きな窓(URLのパラメータや、画面の入力欄)があります。泥棒(攻撃者)は、その窓のすぐ外側に、巧妙に細工した「怪しいおもちゃ(悪意あるJavaScriptコード)」を置いていきました。
本来なら、家の中の人は「怪しいおもちゃだな」と気づいて捨てるべきですよね。しかし、家の中にある自動おもちゃ回収機(脆弱なJavaScriptのプログラム)が、外からやってきたものをろくに確認もせず、そのままリビングの真ん中でスイッチを入れて起動させてしまったのです。
これが、DOMベースXSSの正体です。サーバーを通らず、ブラウザの機能(DOM)をそのまま利用して攻撃が実行されてしまうため、サーバー側の防火壁(WAFなど)をすり抜けてしまう厄介な特徴を持っています。
—
2. ソースからシンクへ:データが流れる危険なルート
ペネトレーションテスト(脆弱性診断)の世界では、この攻撃の道を「ソースからシンク(Sink)へのフロー」と呼びます。
- ソース(Source): ユーザーからの入力やURLなど、外部からデータが入ってくる「入り口」です。(例:
location.searchやlocation.hash) - シンク(Sink): そのデータをそのまま実行してしまったり、画面に表示したりする「危険な出口」です。(例:
eval()やelement.innerHTML)
言葉だけだと難しいので、実際の危ないコードを見てみましょう。以下のJavaScriptを見てください。
// 【危険なコードの例】
// URLのパラメータ(例: ?name=太郎)を取得する
const urlParams = new URLSearchParams(window.location.search);
const userName = urlParams.get('name');
// 取得した値を、確認せずにそのままHTMLの要素にブチ込んでしまう(シンク)
// もしここで userName の中に HTMLタグが含まれていると…大変なことになります!
document.getElementById('greeting').innerHTML = 'こんにちは、' + userName + ' さん!';
もし、攻撃者が次のような怪しいURLをあなたに踏ませたとします。
https://example.com/welcome.html?name=<script>alert('乗っ取り成功!')</script>
上記のコードでは、userName の中に <script>...</script> というタグが丸ごと入り込んでしまいます。それを innerHTML(シンク)が「お、HTMLだな!」と勘違いして実行してしまうため、画面上でポップアップがピロリンと鳴り響き、最悪の場合はブラウザのクッキー(秘密のパスワードのようなもの)を盗み出されてしまうのです。
—
3. 現場で使える!DOMベースXSSの探し方(診断のコツ)
私たちペネトレーションテスターやセキュリティエンジニアは、実際の現場でどのようにこの脆弱性を見つけているのでしょうか?
プロの技といっても、基本は「怪しい場所をしらみつぶしに探すこと」に尽きます。泥棒の足跡を探すように、以下のステップでコードを読み解いていきます。
1. 入り口(ソース)を探す
JavaScriptのコード内で、location.href、location.search、window.name、document.referrer といった「ユーザーが自由にいじれる情報」をどこで読み取っているか、ブラウザの検索機能(Ctrl + F)で探します。
2. データの流れを追う
その読み取ったデータが、加工されずにそのまま変数に代入され、あちこちにたらい回しにされていないか追跡します。
3. 出口(シンク)をチェックする
最終的に、そのデータが以下の危険な命令に渡されていないかを確認します。
element.innerHTMLdocument.write()eval()setTimeout()やsetInterval()(文字列を渡している場合)
もし「ソース」から「シンク」までの間に、怪しいデータを綺麗に洗い流す処理(サニタイズやエスケープ)がされていなければ、そこにDOMベースXSSの vulnerability(脆弱性)が眠っていることになります。
—
4. 一歩ずつ実践しよう!安全なコードへの書き換え
「じゃあ、どうやって直せばいいの?」という話ですよね。安心してください。対策はとてもシンプルです。
先ほどの危険なコードを、安全な形に書き換えてみましょう。
// 【安全なコードの例】
const urlParams = new URLSearchParams(window.location.search);
const userName = urlParams.get('name');
// 1. innerHTML の代わりに textContent を使う
// これにより、たとえ <script> タグが渡されても、ただの「文字(テキスト)」として扱われ、実行されません!
const greetingElement = document.getElementById('greeting');
greetingElement.textContent = 'こんにちは、' + (userName || 'ゲスト') + ' さん!';
ポイント
innerHTML はHTMLタグをそのまま解釈してしまうため危険ですが、textContent や innerText を使えば、入力された文字列を「ただの文字」として安全に画面に表示してくれます。これだけで、多くのDOMベースXSSを防ぐことができます。
—
5. インフラ・サーバー側でできる最後の砦(防御ヘッダー)
クライアント側のJavaScriptを完璧に直すのが理想ですが、人間が書くコードにはどうしてもミスが付きものですよね。そこで、サーバーやインフラの設定でブラウザをガッチリ守る仕組みがあります。それがCSP(Content Security Policy:コンテンツセキュリティポリシー)です。
これは、Webサーバーからブラウザに向けて「うちのサイトでは、こういうスクリプトしか実行しちゃダメだからね!」というルールブックを配る仕組みです。
例えば、HTTPレスポンスヘッダーに以下のような設定を入れます。
# 【CSP設定の例】
# 外部から勝手に読み込まれた怪しいスクリプトの実行をブロックします
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com;
この設定を入れておくと、仮にDOMベースXSSの穴があったとしても、ブラウザが「おっと、このスクリプトは許可されていないリストに入っているぞ!」と察知して、実行をガッチリ阻止してくれます。まさに、家の窓に頑丈な格子を取り付けるような防衛策ですね。
—
まとめ
今回は、DOMベースXSSの仕組みから、データが流れるルート、そして具体的な対策コードやインフラ設定までを見てきました。
- DOMベースXSSは、サーバーを通らずブラウザの中だけで完結する厄介な攻撃。
- ユーザーからの入力(ソース)が、危険な命令(シンク)にそのまま渡らないようにする。
- 画面に文字を表示するときは、
innerHTMLではなくtextContentを使う癖をつける。 - 最後の砦として、CSP(コンテンツセキュリティポリシー)のヘッダーを活用する。
セキュリティ対策は、一度にすべてを完璧にやろうとすると息切れしてしまいます。「まずは自分の書いたコードで innerHTML を使っていないか確認してみよう」といった小さな一歩から、安全なWebの世界を一緒に作っていきましょう!
コメント