« August 2010 | Main | October 2010 »

September 2010

September 23, 2010

String::Filter っていうモジュール書いた - 続: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について

先のエントリ「(Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について」の続き。

弾さんが「404 Blog Not Found:DHTML - 構造化テキストは構造化するのがやっぱ正しい」で示されているような DOM ベースの操作を行えば、原理的に XSS 脆弱性を防ぐことができます。ただ、クライアントサイド JavaScript によるレンダリングはウェブの構造を破壊するという点で筋が悪い(テーブルと FONT タグを利用したページレイアウトが批判されていた頃を覚えていらっしゃいますでしょうか。JavaScript によるレンダリングはウェブのリンク構造も破壊するので一層たちが悪いというのが自分の考え)ですし、サーバサイドでの DOM 操作は重たいので、できれば避けたいところです。

構造化テキストの HTML への変換は、よほど複雑な記法でない限りはワンパスで処理可能ですし、であれば、わざわざ DOM のような構造を作らなくても、SAX のようにイベントベース(あるいはコールバックベース)で処理すれば済むことなのですから。

このような変換の必要は、Twitter に限らず、しばしば耳にするものです。最近自分が目にしたものだと「Template中のURLを自動で賢くアンカーテキストにしたい - すぎゃーんメモ」。このケースでは URL さえリンク化できれば良かったので tokuhirom さんのコメントのとおりでいいんですが、この URI::Find を使う手法だと URL 以外のものも処理したくなった時に対応できないんだよなーと思って見ていたのでした。

そしたらちょうど Twitter の件があったので、複数の変換ルールを合成可能な文字列変換モジュール String::Filter を書いて CPAN にアップロードしました。このモジュールを使えば、ツイートの変換ルールは下の例のように書くことができます。(@user や #hashtag の前には空白が必要といったルールも入っているため)やや長く感じられるかもしれませんが、意図は明確ですし、ルールを後から追加・変更できるので使いやすいのではないでしょうか。

このような HTML スニペット生成手法と、Text::MicroTemplateText::Xslate のような型ベースの自動エスケープ機能(詳細は「Kazuho@Cybozu Labs: Text::MicroTemplate - テンプレートエンジンのセキュリティと利便性」を参照)を備えたテンプレートエンジンを組み合わせることで、自然と、より安全なウェブアプリケーションを開発できるようになっていけばいいな、と思うところです。

use HTML::Entities (encode_entities);
use String::Filter;

# デフォルトの変換ルールを指定してフィルタオブジェクトを生成
my $sf = String::Filter->new(
    default_rule => sub {
        my $text = shift;
        encode_entities($text);
    },
);

# URL の変換ルールを登録
$sf->add_rule(
    'http://[A-Za-z0-9_\\-\\~\\.\\%\\?\\#\\@/]+',
    sub {
        my $url = shift;
        sprintf(
            '<a href="%s">%s</a>',
            encode_entities($url),
            encode_entities($url),
        );
    },
);

# @user の変換ルールを登録
$sf->add_rule(
    '(?:^|\\s)\\@[A-Za-z0-9_]+',
    sub {
        $_[0] =~ /^(.*?\@)(.*)$/;
        my ($prefix, $user) = ($1, $2);
        sprintf(
            '%s<a href="http://twitter.com/%s">%s</a>',
            encode_entities($prefix),
            encode_entities($user),
            encode_entities($user),
        );
    },
);

# #hashtag の変換ルールを登録
$sf->add_rule(
    '(?:^|\\s)#[A-Za-z0-9_]+',
    sub {
        $_[0] =~ /^(.?)(#.*)$/;
        my ($prefix, $hashtag) = ($1, $2);
        sprintf(
            '%s<a href="http://twitter.com/search?q=%s">%s</a>',
            encode_entities($prefix),
            encode_entities(uri_escape($hashtag)),
            encode_entities($hashtag),
        );
    },
);

# tweet を HTML に変換する
my $html = $sf->filter($tweet);

September 22, 2010

(Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について

昨日の Twitter の XSS 騒ぎは、まだ皆さんの記憶に新しいことと思います。いい機会なので、ツイートのような構造化テキストのエスケープ手法について触れておきたいと思います。

Twitter のメッセージは、単なる平文(プレインテキスト)ではなく、「@英数字」のような他のユーザーへの言及と「http://〜」のような URL を自動的にハイパーリンク化する構造化テキストです。

このような複数のルールをもつ構造化テキストを HTML 化する際には、どのようなコードを書けばいいのでしょう? まず「@〜」をリンク化してから、URL をリンク化すればいいのでしょうか? それだと、@〜 のをリンク化した A HREF タグの中の URL がさらにリンク化されていまいますね。

では、URL をリンク化してから @〜 をリンク化すればいいのでしょうか? それだと、@ を含む URL があった場合に、やはり HTML が壊れてしまいます。

正しいアプローチは、全てのルールを同時に適用することです。

Perl で書くなら、以下のように、「@〜」と URL を検出するパターンを単一の正規表現にまとめてトークナイズ (split) し、切り出されたトークンがどのタイプかを判定しながら処理していきます。

my $html = '';
for my $token (split m{(http://[0-9A-Za-z_\.\%\?\#\@/]+|\@[0-9A-Za-z]+)}, $tweet) {
    if ($token =~ m{^http://}) {
        $html .= '<a href="' . encode_entities($token) . '">'
            . encode_entities($token) . '</a>';
    } elsif ($token =~ m{^\@(.*)$}) {
        my $user = $1;
        $html .= '<a href="http://twitter.com/' . encode_entities($user) . '">'
            . encode_entities($token) . '</a>';
    } else {
        $html .= encode_entities($token);
    }
}

でもルールが複雑になってくると面倒なので... 続きはあとで書くString::Filter っていうモジュール書いた - 続: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について」に続く