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

第5回 問題を発生させにくくするURLの扱い方

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

hrefプロパティに相対URLを設定したときに,href以外の各プロパティが正しく取得できない問題を解決する

<a>要素は,絶対URLを指すhrefプロパティだけでなく,locationオブジェクトやURLオブジェクトと同様に,protocolやhost,hostnameといった,URLの部分文字列を指すプロパティもサポートしています。これらのプロパティを用いて,URLをパースして各部分を取り出すことを考えてみましょう。

IEにて,以下のコードのように新たに生成した<a>要素のhrefプロパティにURLを代入し,protocolやhost,hostnameなどのプロパティを読みだしてみます。

// bad code
var elm = document.createElement( "a" );
elm.setAttribute( "href", "http://example.jp/fo" );
console.log( elm.protocol ); // "http:"
console.log( elm.host );     // "example.jp"
console.log( elm.hostname ); // "example.jp"

想定したとおりに正しく動いているように見えるかもしれません。しかし,じつはIEでは,hrefプロパティに相対URLを設定したときに,href以外の各プロパティが正しく取得できないという問題があります。

// bad code
var elm = document.createElement( "a" );

elm.setAttribute( "href", "/foo" );
console.log( elm.protocol ); // IEでは空文字列になる
console.log( elm.host );     // IEでは空文字列になる
console.log( elm.hostname ); // IEでは空文字列になる

elm.setAttribute( "href", "//example.jp/foo" );
console.log( elm.protocol ); // IEでは空文字列になる
console.log( elm.host );     // "example.jp"
console.log( elm.hostname ); // "example.jp"

そこで,IEで<a>要素などを用いてURLを取り扱うという場合には,相対URLをいったん絶対URLに変換する必要があります。相対URLを絶対URLに変換するコードは,先ほどのものがそのまま利用できます。

function getAbsoluteUrl( url ){
    var elm = document.createElement( "a" );
    elm.setAttribute( "href", url );
    return elm.href;
}

// 現在のドキュメントが http://example.jp/ だとする
var url = "/foo";
var elm = document.createElement( "a" );
elm.setAttribute( "href", getAbsoluteUrl( url ) ); // URLを絶対URLに変換しhref属性に設定
console.log( elm.href );     // "http://example.jp/foo"が表示される
console.log( elm.protocol ); // "http:"が表示される
console.log( elm.hostname ); // "example.jp" が表示される

相対URLを絶対URLに変換する際に,ベースとなるURLを明示的に指定できない問題を解決する

さらに,<a>要素を用いて相対URLを絶対URLに変換する際には,常に現在のURL(location.href)を起点として変換されてしまい,ベースとなるURLを明示的に指定できないという問題もあります。

これを解決するためには,以下のような方法を採ります。

  1. IE11ではDOMParserを,IE10ではdocument.implementation.createHTMLDocumentを用い,現在表示されているコンテンツとは切り離されたDOMツリーを作成する
  2. そのDOMツリーに<base>要素を追加し,ベースURLを指定する
  3. そのDOMツリー内で前述の<a>要素を使った方法を用いて相対URLを絶対URLに変換する

具体的なコードは以下のようになります。

var createUrl = (function() {
    var doc;

    try {
        new URL("/", "http://example.jp/"); // URLコンストラクタが使えるかのテスト
    } catch (e) {
        try {
            doc = (new DOMParser).parseFromString("<html><head></head><body></body></html>", "text/html");
        } catch (e) {
            doc = document.implementation.createHTMLDocument("");
        }
    }

    return function (url, base) {
        var d = document, baseElm, aElm, result;
        if (doc === undefined) {
            if (base === undefined) {
                base = location.href;
            }
            result = new URL(url, base);
        } else {
            // URLコンストラクタが使えないため<a>要素を使ってURLを解決する
            if (base !== undefined) {
                // baseが指定されている場合は現在のdocumentとは切り離されたHTMLDocument内に<base>要素と<a>要素を設定
                d = doc; 
                while (d.head.firstChild) d.head.removeChild(d.head.firstChild);
                baseElm = d.createElement("base");
                baseElm.setAttribute( "href", base );
                d.head.appendChild( baseElm );
            }
            aElm = d.createElement("a");
            aElm.setAttribute("href", url);
            aElm.setAttribute("href", aElm.href);
            //d.appendChild(aElm);

            result = {
                protocol: aElm.protocol,
                host: aElm.host,
                hostname: aElm.hostname,
                port: aElm.port,
                pathname: aElm.pathname,
                search: aElm.search,
                hash: aElm.hash,
                href: aElm.href,
                username: "",
                password: "",
                origin : aElm.origin || null
            };
            if (result.protocol === "http:" && result.port === "80") {
                // httpかつデフォルトポートの場合はポート番号の"80"を削除
                result.port = "";
                result.host = result.host.replace( /:80$/, "" );
            }else if (result.protocol === "https:" && result.port === "443") {
                // httpsかつデフォルトポートの場合はポート番号の"443"を削除
                result.port = "";
                result.host = result.host.replace( /:443$/, "" );
            }
            if (result.protocol === "http:" || result.protocol === "https:") {
                if (result.pathname && result.pathname.charAt(0) !== "/") {
                    // pathnameの先頭の"/"が付与されないバグへの対応
                    result.pathname = "/" + result.pathname;
                }
                if (!result.origin) {
                    result.origin = result.protocol + "//" + result.hostname + (result.port ? ":" + result.port : "");
                }
            }
        }
        if (result.username || result.password) {
            // URLにBasic認証のユーザ名,パスワードが含まれている場合は例外を発生させる
            throw new URIError( result.username || result.password );
        }
        return result;
    };
})();

var url = createUrl( "/foo?bar", "http://example.jp/baz" );
console.log( url.href );    // "http://example.jp/foo?bar"
console.log( url.search );  // "?bar"

このcreateUrlという関数は,これまで説明したとおりURLコンストラクタが利用可能な場合にはそれを利用し,利用できない場合には<a>要素を使いつつ,相対URLの場合にはDOMParserまたはcreateHTMLDocumentを用いて絶対URLに変換します。そして,戻り値として,locationやURLオブジェクトと同様に,URLをパースした結果を各プロパティにセットしたオブジェクトを生成して返します。

また,先に説明したように,URLにBasic認証のユーザー名やパスワードが含まれている場合にはセキュリティ上の問題を引き起こすことがあるので,ユーザー名やパスワードが含まれるURLの場合には例外を発生するようになっています。

まとめ

今回は,URLを文字列として自前でパースするのではなく,ブラウザの機能を利用してパースする方法を解説しました。このような方法をとることで,少ないコードで,実際のブラウザが解釈するとおりのURLのパース結果を得ることができます。

繰り返しになりますが,JavaScriptのコード上において,URLを基準としてセキュリティ上の判断を行う場面は,開発者が意図していない場合も含めると,比較的頻繁に発生します。そういった場面で,URLを文字列として取り扱い,自身でURLをパースするコードを書くことは,多くの場合に脆弱性を発生させる原因になります。よほどの理由がない限り,URLをパースするには,自身でコードを書くのではなく,ブラウザの機能を用いるようにしましょう。

また,今回説明したようなURLのパース時の問題以外にも,JavaScript上のコードからURLを操作する際にlocationオブジェクトなどを通じてセキュリティ上の問題が発生することがよくあります。locationオブジェクトの操作に起因する脆弱性としてよく見かけるのは以下のようなものです。

  • location.hrefにjavascript:alert("xss");やvbscript:MsgBox "xss"といったスクリプトのスキームを代入されることによりXSSが発生する
  • HTML内にlocation.hrefやlocation.hashを表示する際に,URL内に含まれる「<」⁠>」などがエスケープされていないことが原因でXSSが発生する

このような脆弱性の具体例や対策の詳細についても,今後回を追って取り上げたいと思います。

著者プロフィール

はせがわようすけ

株式会社セキュアスカイ・テクノロジー常勤技術顧問。 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/