2010年09月22日 21:30 [Edit]

構造化テキストの間違ったエスケープ手法について

昨晩のtwitter XSS祭りは、ふだんもtwitter.comは使わない私には遠くの祭り囃子だったのですが、せっかくの自戒の機会なので。

Kazuho@Cybozu Labs: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について
正しいアプローチは、全てのルールを同時に適用することです。

これは残念ながら(おそらく)必要条件であっても十分条件ではありません。


こういう(かなりええかげんな)正規表現でtweetをparseしていたとします。

re_http = '(?:https?://[\\x21-\\x7e]+)';
re_user = '(?:[@][0-9A-Za-z_]{1,15})';
re_hashtag = '(?:[#]\\S+)';
re_entities = '(?:[<>&])';
re_tweet = new RegExp([re_http, re_user, re_hashtag, re_entities].join('|'), 'g');

ダメな例

以下は全てのルールを同時に適用していますが、それでもやっぱりXSSとなります。

parse_tweet_wrong = function(s){
    return s.replace(re_tweet, function(m0){
        if (m0.match(/^[<>&]$/)){
            return {'<':'&lt;','>':'&gt;','&':'&amp;'}[m0];
        }if (m0.match(/^http/)){
            return '<a href="' + m0 + '">' + m0 + '</a>';
        }else if(m0.match(/^\@/)) {
            return '@<a href="http://twitter.com/'
                + m0.substr(1) + '">' + m0.substr(1) + '</a>';
        }else if(m0.match(/^\#/)) {
            return '<a href="http://twitter.com/search?q='
                + encodeURIComponent(m0) + '">' 
                + m0 + '</a>';
        }else{
            return m0;
        }
    });
};
Tweet:
Parsed:

例文の最後のリンクは、<a href="http://twitter.com/#@"onmouseover="alert(location.href)"/">…</a>と展開されてしまうのですね。href"が閉じちゃっているわけです。また、上記の正規表現は、Mutually Exclusiveではありません。URIをひっかける正規表現が、他の正規表現を「飲み込んで」しまうということです。

改善例

要はhrefの中に、"があろうがなかろうがすべて入ってしまえば、「ただの404なリンク」になるのですから、そうすればよいのです。kazuhoさんのsnippetは(おそらくHTML::Entitiesの)encode_entities()でくくっているのでこの点をもきちんと満たしているのですが、JavaScriptではどうすればよいでしょう?

こんな手が使えます。


make_link = function(href, text) {
    var a = document.createElement('a');
    a.href = href;
    a.appendChild(document.createTextNode(text));
    var div = document.createElement('div');
    div.appendChild(a);
    return div.innerHTML;
};

parse_tweet_right = function(s) {
    return s.replace(re_tweet, function(m0) {
        if (m0.match(/^[<>&]$/)){
            return {'<':'&lt;','>':'&gt;','&':'&amp;'}[m0];
        }else if (m0.match(/^http/)) {
            return make_link(m0, m0);
        }else if (m0.match(/^\@/)) {
            return '@' + make_link(
                'http://twitter.com/' + m0.substr(1), m0.substr(1)
            );
        }else if (m0.match(/^\#/)) {
            return make_link(
                'http://twitter.com/search?q=' + encodeURIComponent(m0), m0
            );
        }else {
            return m0;
        }
    });
};
Tweet:
Parsed:

きちんとDOM Elementを作った上で中身を取り出しているというわけです。

正規表現が("を引っ掛けてしまうような)ええかげんなものでも、これでXSSが防がれています。

別のいい方をすれば、

正しいアプローチその二は、いったんきちんと構造を作る

ということになるでしょうか。

Dan the Structured Programmer


この記事へのトラックバックURL

この記事へのトラックバック
最近twitterでXSSによる攻撃があって大騒ぎだったというのがよく話題になっているようです。 自分も実は某社でwikiのような記法を使って記事が書けるwebサイトを作っているのですが、よく考えないで正規表現でごりごり実装したのでXSSを連発して苦い経験をしました。 そこで...
構造化テキストと文法と言語【進・日進月歩】at 2010年09月23日 16:17
うーん、気に食わない。 404 Blog Not Found:構造化テキストの間違ったエスケープ手法について make_link = function(href, text) { var a = document.createElement('a'); a.href = href; a.innerHTML = text; var div = document.createElement('div...
構造化テキストは構造化するのがやっぱ正しい【404 Blog Not Found】at 2010年09月23日 02:13
この記事へのコメント
^$が抜けてました。fixed. Thanks!

Dan the Blogger Hereof
Posted by dankogai at 2010年09月23日 04:40
URLに&を含むとundefinedになりますね
Posted by os0x at 2010年09月23日 02:03
半分は(re_httpが粗悪な点に対する)意図もなくもなかったのですが、元のtwitterもこの穴はなかったということで翻意してタグをencode_entities()相当もするようにしました。thanks!

Dan the Blogger Hereof
Posted by dankogai at 2010年09月23日 00:32
この例では「Tweet」に「<script>alert(document.cookie)</script>」と入れて「parse」を実行しただけでそのスクリプトが実行されてしまいますが、これは意図した動作でしょうか?
Posted by http://www.hatena.ne.jp/nanto_vi/ at 2010年09月22日 23:41