JavaScriptセキュリティの基礎知識

第8回 DOM-based XSS その3

この記事を読むのに必要な時間:およそ 7 分

ユーザーからの入力に基づき,自由にHTMLを生成する

テキスト装飾用のHTMLを埋め込むためのリッチテキストエディタや,MarkdownからHTMLへの展開をブラウザ上で行う場合など,アプリケーションの種類によっては,ユーザーからの入力に基づき自由にHTMLを生成する処理をJavaScriptによって実現することを求められる場合があります。

通常,こういった複雑な処理をまったくのゼロから開発することは稀で,一般的にはサードパーティ製のリッチテキストエディタライブラリやMarkdown展開のためのライブラリを使用することになるでしょう。たとえば,MarkdownからHTMLへの展開にmarked.jsを使用する場合,コードは以下のようになります。

<script src="lib/marked.js"></script>
...
<div id="content"></div>
<script>
    var markdownText = "# title\n\n-list 1\n-list 2\n";
    document.getElementById("content").innerHTML = marked(markdownText);
</script>

このとき,変数markdownTextに「<img src=# onerror=alert(1)>」のようなHTMLおよびJavaScript文字列が含まれていると,Markdownから生成されたHTMLにもその内容がそのまま展開されてしまい,変数markdownTextが攻撃者によってコントロール可能な場合にはXSSにつながります。

じつは,marked.jsではsanitizeオプションを指定することで,Markdownとして定められた構文のみを解釈し,任意のHTMLタグの生成を抑制することができます。

// 自由なHTMLの生成を抑制
var markdownText = "# title\n\n<img src=# onerror=alert(1)>\n<img src='/img/fig.png'>";
document.getElementById("content").innerHTML = marked(markdownText,{sanitize:true}); // <img>はどちらも生成されない

ただし,この場合にはユーザーは任意のHTMLタグを使うことができず,Markdownの限られた書式のみしか利用できないことになり,ユーザーにとってみると作成できるHTMLの自由度が下がることになります。また,marked.jsのsanitizeオプションを指定した場合に本当に安全上の問題点がないかを確認する必要もあります。marked.js以外のライブラリを使用したいと思った場合には,同種のオプションが存在しない可能性もあります。

そこで,生成されるHTMLの自由度は最大限に保ちつつ,XSSを発生させない方法を考えましょう。

このような場合に採れる方法として,⁠DOMParser APIやcreateHTMLDocumentを使用してHTML文字列をパースし,それにより構築されたDOMツリーから許可されたタグと属性のみを取り出す」というものが知られています第4回 危険性が理解されにくいネイティブアプリ内XSS(2⁠⁠:フロントエンドWeb戦略室⁠。

DOMParser APIやcreateHTMLDocument APIは,最近のブラウザに搭載されているAPIで,ブラウザが現在表示しているdocumentに影響を与えることなく,与えられた文字列をパースして,DOMツリーを構築することができます。これらのAPIを使用して,構築されたDOMツリーに対し,そのDOMツリー内のすべてのノードおよび属性を列挙し,それらのうち許可されたタグおよび属性以外をすべて除外するのです。

DOMParserを使ってDOMツリーを構築し,許可されたタグと属性のみを取り出すコードの例を以下に示します。

function safeHtml (htmlString) {
    var parser, doc, body, i, newNode, parentNode, buildNode;
    parser = new DOMParser();
    doc = parser.parseFromString(htmlString, "text/html");
    body = doc.body;
    parentNode = document.createElement("div");

    buildNode = function (node) {
        var i, elm, childNode, attrName, attrValue;

        switch (node.nodeType) {
        case 1: // ELEMENT_NODE
            if (node.tagName === "DIV" || node.tagName === "IMG") {
                elm = document.createElement(node.tagName); 
                if (node.tagName === "IMG") {
                    for (i=0; i<node.attributes.length; i++) {
                        attrName = node.attributes[i].name;
                        attrValue = node.attributes[i].value;
                        if (attrName === "src" || attrName === "title" || attrName === "alt") {
                            elm.setAttribute( attrName, attrValue );
                        }
                    }
                }
                for (i=0; i<node.childNodes.length; i++) {
                    console.log(node.childNodes[i]);

                    childNode = buildNode(node.childNodes[i]);
                    if (childNode !== undefined) {
                        elm.appendChild( childNode );
                    }
                }
            }
            break;
        case 3: // TEXT_NODE
            elm = document.createTextNode(node.textContent);
            break;
        }
        return elm;
    };

    for (i=0; i <body.childNodes.length; i++) {
        newNode = buildNode(body.childNodes[i]);
        if (newNode !== undefined) {
            parentNode.appendChild(newNode);
        }
    }
    return parentNode.innerHTML;
}

unsafeHtml = '<div>Hello, Sanitize!<img src=# alt="incorrect image" onerror=alert(1)></div>';
sanitizedHtml = safeHtml(unsafeHtml);
elm.innerHTML = sanitizedHtml; // <div>Hello, Sanitize!<img src="#" alt="incorrect image"></div>

この例では,DOMParserによって構築されたDOMツリーの子要素および属性を列挙し,divタグ,imgタグ,テキストノードだけを許可,さらにimg要素に関してはsrc属性,title属性,alt属性のみを許可し,それら以外を除去した安全なHTML文字列を返しています。

たとえば,以下の文字列を与えた場合には,div要素,img要素,テキストノードおよびimg要素内のsrc属性,alt属性は保持されますが,img要素内のonerror属性は削除されます。

<div>Hello, Sanitize!<img src=# alt="incorrect image" onerror=alert(1)></div>

結果として,以下のHTML文字列を得ることができます。

<div>Hello, Sanitize!<img src="#" alt="incorrect image"></div>

実際にこのような方法で安全なHTMLを組み立てるためのライブラリの実装としては,Mario Heiderich氏率いるCure53によるDOMPurifyなどがあります。⁠ユーザーからの入力に基づき自由にHTMLを組み立てつつも,生成されるHTMLの安全性は確保したい」という場合には,こういったライブラリの使用を検討するといいでしょう。

著者プロフィール

はせがわようすけ

株式会社セキュアスカイ・テクノロジー常勤技術顧問。 Internet Explorer,Mozilla FirefoxをはじめWebアプリケーションに関する多数の脆弱性を発見。 Black Hat Japan 2008,韓国POC 2008,2010,OWASP AppSec APAC 2014他講演多数。 OWASP Kansai Chapter Leader / OWASP Japan Board member。

URL:http://utf-8.jp/