なぜ「javascript:」スキームは今もなお開発者の盲点なのか
現場でコードレビューをしていると、今でもこの手の脆弱性に遭遇する。「そんなの昔の技術じゃないのか?」と思うかもしれないが、甘い。攻撃者は常に「開発者がHTMLの仕様を正しく理解していないポイント」を突いてくる。
特に タグの href 属性や の src 属性に、ユーザーから受け取ったURLをそのままぶち込む実装。これ、実は一番手軽で、かつ最も被害が甚大なXSSの入り口だ。
「javascript:」スキームによる攻撃の裏側(PoC)
例えば、ユーザーのプロフィールページに「外部サイトリンク」を設定できる機能があったとしよう。多くのエンジニアは、DBから取得したURLを単にエスケープして出力するだけで満足してしまう。
ブラウザはこの href 属性を評価する際、javascript: というプロトコルを見つけると、その後の文字列をJavaScriptコードとして実行する。これこそが「URLスキームによるXSS」だ。
反射型だろうが格納型だろうが関係ない。ユーザーがリンクをクリックした瞬間に、攻撃者はセッションハイジャックに必要なトークンを奪取したり、勝手にPOSTリクエストを飛ばして設定を書き換えたりする。「自分のブラウザで実行されるだけ」と高をくくっていると、バックエンドの管理機能まで乗っ取られることになるぞ。
—
「安易なフィルタリング」が失敗する理由
よくある間違いが、javascript という文字列を置換で消そうとすることだ。
java script:alert(1)(改行コードの挿入)JavAscrIpt:alert(1)(大文字小文字の混在)
こういった難読化手法を全て正規表現で追いかけるのは、セキュリティのプロでも骨が折れる。「ブラックリスト方式」で守ろうとするな。 泥沼にはまるだけだ。
—
正解:プロトコルホワイトリストによる防御
防御の鉄則は「ホワイトリスト方式」だ。許可するスキームを http と https のみに制限する。これだけで javascript: などの危険なスキームを完全に無効化できる。
Python (Flask/Django等のバックエンド) での実装例
ライブラリを使うのが最も確実だ。Pythonなら urllib.parse を使って、パースした結果のスキームを確認するのが鉄則だ。
from urllib.parse import urlparse
def is_safe_url(url):
# 許可するスキームを定義
allowed_schemes = {‘http’, ‘https’}
parsed = urlparse(url)
# スキームが空(相対パス)または許可されたスキームのみを通す
# 攻撃者は ‘javascript:’ を指定してくるので、ここを通らない
return parsed.scheme in allowed_schemes or parsed.scheme == ”
利用例
user_input = “javascript:alert(1)”
if is_safe_url(user_input):
render_link(user_input)
else:
render_link(“#”) # 安全なデフォルト値へ置換
JavaScript (フロントエンド) での実装例
フロントエンドで動的にリンクを生成する場合も同様だ。
function getSafeUrl(url) {
try {
const parsed = new URL(url);
// httpかhttps以外なら例外を投げる
if ([‘http:’, ‘https:’].includes(parsed.protocol)) {
return url;
}
} catch (e) {
// パースできない不正な形式は無視
}
return ‘#’; // 安全なフォールバック
}
// 使用例:DOM操作時
const link = document.createElement(‘a’);
link.href = getSafeUrl(userInput);
—
インフラ層での二段構え:CSPの活用
コードレベルでの修正が完了したとしても、人間が書くコードにミスはつきものだ。だからこそ、Content Security Policy (CSP) をヘッダーで設定しておくことを強く勧める。
NginxでCSPヘッダーを付与する場合の設定例だ。
CSP設定例
‘unsafe-inline’ を排除し、信頼できないソースからのJS実行を抑制する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’;”;
これにより、万が一コードの隙を突かれて javascript: スキームが注入されても、ブラウザが「このリンクの実行はポリシー違反だ」と判断し、実行をブロックしてくれる。
—
チーフからのアドバイス:実装の極意
最後に、チームのみんなへ。
「機能を作る」ことと「安全に動くものを作る」ことは、エンジニアにとって別物ではない。URLを扱うときは、必ず「これは外部からの入力か?」「プロトコルは信頼できるか?」を自問自答すること。
今回紹介したホワイトリスト方式は、実装コストは低いが効果は絶大だ。今日から、君たちのプロジェクトにある全ての href や src 属性を見直してほしい。もし「とりあえずそのまま表示している」箇所があれば、それが次回のインシデントの火種になる。
セキュリティは、派手なハッキング手法を防ぐことよりも、こうした「当たり前のチェック」をどれだけ妥協せずに積み重ねられるかにかかっている。頼んだぞ。
コメント