Skip to main content

JavaScriptにおけるDOM clobbering攻撃の軽減

著者

Keshav Malik

2023年8月7日

0 分で読めます

Document Object Model(DOM)はHTMLとJavaScriptをつなぐインターフェースとして機能し、静的なコンテンツと動的なインタラクティブ性の橋渡しをします。この機能により、DOMは現代のWeb開発に欠かせないものとなっています。

しかし、DOMには落とし穴があります。それがDOM clobberingです。DOM clobberingは、HTML要素がグローバルなJavaScript変数や関数と競合すると発生し、予期しない動作やWebアプリケーションのセキュリティ上の抜け穴につながる可能性があります。

この問題に対処するには、慎重なプログラミング、JavaScriptに関する十分な知識、そしてWebアプリケーションセキュリティへの深い理解が必要です。この記事では、DOM clobberingの仕組みを解説し、より安全で堅牢なWebアプリケーションを構築するための対策を紹介します。

DOM clobberingが発生する仕組み

すべてのHTML要素には、JavaScriptから特定の要素を簡単に参照できるよう、一意のID属性またはname属性を設定できます。たとえば、次のHTMLボタンには"myButton"という値のID属性が設定されています。

<button id="myButton">Click Me!</button>

このIDをJavaScriptコードで使うと、たとえばクリック時にテキストを変更するなど、ボタンを操作できます。

document.getElementById('myButton').onclick = function() {
    this.textContent = 'Clicked!';
};

DOMによって、HTMLとJavaScriptのこのような連携が可能になります。しかし、HTML要素のID属性やname属性が、グローバルなJavaScript変数や関数と競合したらどうなるでしょうか。

ブラウザーがHTMLページを読み込むと、HTML DOM内のすべてのID属性とname属性に対して、グローバルなJavaScript変数を自動的に作成します。"myButton"という名前のHTML要素があると、ブラウザーはそのHTML要素を参照するグローバルなJavaScript変数myButtonを作成します。

ここで、myButtonという名前のJavaScript関数を宣言した場合を考えてみましょう。

function myButton() {
    // some code
}

さらに、IDまたはnameが"myButton"のHTML要素もあるとします。ページの読み込み時に、ブラウザーの自動処理によってJavaScript関数myButtonがHTML要素への参照で上書きされます。

<button id="myButton">Click Me!</button>

console.log(myButton); // This will log the HTML button element, not the function

これがDOM clobberingであり、予測できない動作やセキュリティ脆弱性を引き起こす可能性があります。攻撃者が属性を操作できる場合、Webページに悪意のあるコードを注入し、クロスサイトスクリプティング(XSS)のようなセキュリティ問題を引き起こすおそれがあります。

例を見てみましょう。

Enter your name: <input id="username" type="text">
<button onclick="greet()">Greet</button>

function greet() {
    var username = document.getElementById('username').value;
    alert(`Hello ${username}`);
}

攻撃者は<img id='alert' src=x onerror='alert(1)'>のようなコードを入力して、'id'が'alert'の新しいHTML要素を作成できます。この要素は、JavaScriptの通常のalert関数を置き換える、つまりclobberします。次にWebサイトがこの関数を使おうとしたとき、正常に動作しないばかりか、悪意のあるコードが実行される可能性もあります。

DOM clobberingの脆弱性を特定する

ユーザーが名前とフィードバックメッセージを入力して送信する、基本的なWebアプリケーションを考えてみましょう。ページにはフィードバックが表示されます。

HTML:

<h2>Feedback Form</h2>
<form>
  <label for="name">Name:</label><br>
  <input type="text" id="name" name="name"><br>
  <label for="feedback">Feedback:</label><br>
  <textarea id="feedback" name="feedback"></textarea><br>
  <input type="submit" value="Submit">
</form>

<div id="feedbackDisplay"></div>

JavaScript:

document.querySelector('form').onsubmit = function(event) {
    event.preventDefault();

    let name = document.getElementById('name').value;
    let feedback = document.getElementById('feedback').value;

    let feedbackElement = document.getElementById('feedbackDisplay');

    feedbackElement.innerHTML = `<p><b>${name}</b>: ${feedback}</p>`;
};

このコードはユーザーの名前とフィードバックを受け取り、feedbackDisplayのdiv内にある段落要素に表示します。

攻撃者はフィードバックフォームにHTMLを送信して、このコードを悪用できます。たとえば、nameフィールドに次のコードを入力してフォームを送信すると、フィードバック表示領域がscriptタグに置き換わります。

<script id="feedbackDisplay">window.location.href='http://maliciouswebsite.com';</script>

フォームが次のフィードバックを表示しようとすると、代わりにスクリプトが実行され、ユーザーは悪意のあるWebサイトにリダイレクトされます。

このシナリオで脆弱性が生じるのは、アプリケーションがユーザー入力からHTMLを生成し、既存の要素と同じIDを持つscriptタグを作成するためです。これは深刻な影響をもたらすDOM clobberingの一例です。攻撃者はユーザーのブラウザーを制御し、機密データを盗んだりマルウェアをインストールしたりできる可能性があります。

DOM clobberingを防ぐためのセキュアコーディングの実践

こうした脆弱性への理解を深めたところで、DOM clobberingのリスクを軽減するためのベストプラクティスを見ていきましょう。

変数と関数のスコープを適切に設定する

DOM clobberingのよくある原因の一つは、JavaScriptのグローバルスコープの誤用です。特定のスコープ内で変数や関数を定義すれば、上書きの範囲をそのスコープまたはネストされたスコープに限定でき、競合のリスクを最小限に抑えられます。

JavaScriptのスコープ規則を適用して、先ほどの例を書き換え、方法を見てみましょう。

(function() {
    // All variables and functions are now in this function's scope
    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };
})();

注:上記のコードブロックでは、HTMLのサニタイズにDOMPurifyを使用しています。Node.jsではnpm install dompurifyでインストールできます。HTMLには<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>を追加してください。

このコードでは、すべてを即時実行関数式(IIFE)で囲み、新しいスコープを作成しています。formとfeedbackElementの変数、およびonsubmitイベントハンドラーに割り当てられた関数はグローバルスコープにないため、clobberされません。

一意の識別子を使う

Webページ上のすべての要素に一意のIDを設定すると、重要な関数や変数を誤って上書きするリスクを減らせます。また、一般的な名前や、グローバルなJavaScriptオブジェクトや関数と競合する可能性のある名前は避けましょう。

グローバル名前空間の汚染を避ける

グローバル名前空間をクリーンに保つことは、安全なJavaScriptを書くうえで重要です。グローバルスコープ内の変数や関数が増えるほど、DOM clobberingのリスクも高まります。JavaScriptの関数スコープやES6のブロックスコープを使い、変数や関数をその範囲内に収めましょう。後者を使った例を紹介します。

    // All variables and functions are now in this block's scope
    let form = document.querySelector('form');
    let feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        let name = document.getElementById('name').value;
        let feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        let newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

このコードでは、{}で定義されるブロックを使って新しいスコープを作成しています。すべての変数と関数はこのブロック内に限定され、グローバルスコープには置かれません。

注:このガイドのコード例は説明用に簡略化したものであり、本番のWebサイトやアプリでの使用を意図したものではありません。

JavaScriptの機能を使ってDOM clobberingを軽減する

最新のJavaScriptには、DOM clobberingのリスクを最小限に抑えるのに役立つ機能がいくつかあります。特に、ES6で導入されたletとconstキーワードを使うと、変数宣言をより適切に制御できます。

従来、JavaScriptの変数宣言にはvarキーワードを使ってきました。しかしvarにはいくつかの注意点があり、その一つがブロックスコープを持たず、関数スコープとグローバルスコープしかないことです。そのため、varで宣言した変数は、宣言したブロックの外からアクセスして上書きできます。

一方、letとconstはいずれもブロックスコープを持ち、宣言されたブロック内でのみアクセスできます。この特性により、変数が上書きされる可能性を抑えられるため、変数宣言にはこちらが適している場合が多くあります。

constを使って定数を宣言することもできます。定数は、一度代入すると変更できない値です。重要な変数が意図せず変更されるのを防ぐうえでも役立ちます。

DOM clobberingを防ぐため、フィードバックフォームのコードでこれらの機能を使う方法を見てみましょう。

    const form = document.querySelector('form');
    const feedbackElement = document.getElementById('feedbackDisplay');

    form.onsubmit = function(event) {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const feedback = document.getElementById('feedback').value;

        // Sanitize user input
        name = DOMPurify.sanitize(name);
        feedback = DOMPurify.sanitize(feedback);

        const newFeedback = document.createElement('p');
        newFeedback.textContent = `${name}: ${feedback}`;
        feedbackElement.appendChild(newFeedback);
    };

このコードでは、varを使っていた箇所をすべてconstに置き換えました。すべての変数を宣言したブロック内に限定し、定数は上書きできないようにしています。

letとconstを使ってもDOM clobberingのリスクがなくなるわけではありませんが、セキュアコーディングの重要な要素であることに変わりはありません。こうしたJavaScriptの機能を理解して実践することで、Webアプリケーションの安全性を高められます。

DOM clobberingの脆弱性を監視・検出する

セキュアコーディングの実践はDOM clobberingの予防に役立ちますが、脆弱性が発生した際に検出することも同じくらい重要です。役立つツールや手法を見ていきましょう。

静的コード解析

Snyk Codeのような静的アプリケーションセキュリティテスト(SAST)ツールは、DOM clobberingを含む潜在的なセキュリティ脆弱性を効果的に検出できます。こうしたツールはソースコードを実行せずに解析し、セキュリティ問題につながる可能性のあるパターンを特定します。JavaScriptを包括的にカバーするSnykは、Web開発者にとって優れた選択肢です。

ブラウザーの開発者ツール

ChromeやFirefoxなどで利用できるブラウザーの開発者ツールは、DOM clobberingの脆弱性を検出するうえで強力な味方になります。

これらのツールを使って潜在的な問題を特定する簡単な例を見てみましょう。

  1. Webページ上の任意の場所を右クリックし、検証を選択して開発者ツールを開きます。WindowsではCtrl+Shift+I、macOSではCmd+Option+Iを使うこともできます。

  2. JavaScriptのエラーやログが表示されるConsoleタブに移動します。

  3. コンソールに「window」と入力してEnterを押し、すべてのグローバル変数と関数を含むグローバルスコープを調べます。特に、HTML要素のIDやnameと同じ名前の変数など、不自然なものがないか確認しましょう。

  4. ElementsタブでページのHTMLを編集し、DOMを操作して潜在的な脆弱性をテストします。たとえば、グローバル変数や関数と同じIDを持つ要素を追加し、上書きされるかどうかを確認できます。

まとめ

この記事では、DOM clobberingが発生する仕組みと、それに伴う危険性、そして対策を解説しました。適切な変数スコープの設定、一意の識別子の使用、JavaScriptのletとconstの適切な選択が、この脆弱性の軽減に不可欠です。

コードの安全性を維持するには、予防と検出の両方に取り組む必要があります。推奨されるコーディング手法やブラウザーの開発者ツール、SnykのようなSASTソリューションを活用することで、既存および潜在的な問題を特定できます。

こうした手法をJavaScriptアプリケーションに取り入れ、Webアプリケーションセキュリティの最新動向を把握することで、セキュアなコーディングを継続的に実践できます。

JavaScriptのセキュリティを守るSnyk

最初のコードから最後のnpm依存関係まで、SnykはIDE、CLI、GitのワークフローからJavaScriptアプリケーションを安全に保ちます。

続きを読む

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。

feature insights context
Blog

予防は、本質的に解決済みの問題なのでしょうか?

エージェントが生成するコードの予防策はアーキテクチャ上解決されていますが、開発を遅らせることなくセキュリティを守る制御を選ぶことが、依然として課題です。