文字エンコーディングとセキュリティ攻撃の関係
現在、書籍「安全なWebアプリケーションの作り方」を読んでいるのですが、一部調べながら読んでいるので、調べるのであればせっかくなので記事にしてアウトプットしようと思いますヽ(`▽´)/
まずは、文字エンコーディングによるセキュリティ攻撃ができるらしいんですが、どんな攻撃だろうとし調べてみました✨️
具体的な攻撃例
1. UTF-7によるXSS攻撃
最も有名な例が「UTF-7を使ったXSS攻撃」です。
シナリオ
- Webアプリケーションは「UTF-8だけを受け付ける」と想定している
- しかし、文字エンコーディングのチェックをしていない
- 攻撃者がUTF-7で悪意のあるスクリプトを送信する
具体例
通常のXSS: <script>alert('XSS')</script>
UTF-7版: +ADw-script+AD4-alert('XSS')+ADw-/script+AD4-
UTF-7では < が +ADw-、> が +AD4- と表現されます。
何が起こるか
- アプリケーションは「
<script>というタグはないな」とチェックをすり抜ける - でもブラウザがUTF-7として解釈すると、実際には
<script>タグとして実行される - XSS攻撃が成功してしまう
2. 不正な文字列の混入(Shift_JIS問題)
まず基礎知識:文字はコンピュータ内部では数字で表現される
ASCIIコード(1バイト文字)の例:
'A' = 0x41
'\' = 0x5C ← バックスラッシュ
''' = 0x27 ← シングルクォート
Shift_JIS(2バイト文字)の例:
'表' = 0x95 0x5C ← 注目!2バイト目が0x5Cで'\'と同じ!
'ソ' = 0x83 0x5C ← これも2バイト目が0x5C
問題の核心
Shift_JISの「表」という文字は、内部的には「0x95 0x5C」という2つの数字で表現されます。ここで重要なのは、2バイト目の「0x5C」がバックスラッシュ \ と全く同じコードだということです。
攻撃シナリオを段階的に説明
ステップ1:正常な処理(SQLインジェクション対策あり)
// ユーザーが悪意のある入力をする
$input = "' OR '1'='1";
// 対策:エスケープ処理をする('を\'に変換)
$escaped = addslashes($input); // → \' OR \'1\'=\'1
$sql = "SELECT * FROM users WHERE name = '$escaped'";
// 結果:安全! 'が\'になっているので攻撃失敗
ステップ2:Shift_JISを使った攻撃
// 攻撃者が「表」+ 悪意のある文字列を送る
$input = "表' OR '1'='1"; // Shift_JISで送信
// 内部コード:[0x95 0x5C] [0x27] [0x20] [0x4F] [0x52]...
// └「表」┘ └'┘ └空白以降┘
// エスケープ処理を実行
$escaped = addslashes($input);
// ここで何が起こるか?
ステップ3:エスケープ処理の誤動作
// addslashes関数は'を見つけると、その前に\を追加する
// しかし、Shift_JISの「表」の2バイト目(0x5C)を
// 「バックスラッシュ」だと誤認識してしまう可能性がある
// 期待される結果:表\' OR \'1\'=\'1
// 実際の結果 :表\' OR '1'='1
// ↑ここの'がエスケープされない!
ステップ4:SQL文の実行
$sql = "SELECT * FROM users WHERE name = '表\' OR '1'='1'";
// 「表\」という文字列として認識される
// その後の OR '1'='1' がSQL条件として実行される
// → SQLインジェクション成功!
図解:
攻撃前:SELECT * FROM users WHERE name = '表' OR '1'='1'
↑ ↑
エスケープしたい'
エスケープ誤動作:
「表」= [0x95][0x5C] ← この0x5Cを\だと誤認
↓
[0x95][0x5C][0x5C] ← \を追加してしまう
↓
実際の結果:[0x95][0x5C][0x5C][0x27] OR...
└「表」┘└追加された\┘└'┘
↑この'がエスケープされない!
対策
// 文字エンコーディングを考慮したエスケープ関数を使う
$escaped = mysqli_real_escape_string($connection, $input);
// または、プリペアドステートメントを使う(推奨)
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$input]);
3. ヌルバイト攻撃(オーバーロングUTF-8)
まず基礎知識:ヌルバイトとは?
ヌルバイト(\0または0x00)は、多くのプログラミング言語で「文字列の終わり」を意味する特殊な文字です。
// 例:ファイル名の途中にヌルバイトがあると...
$filename = "evil.php\0.txt";
// 多くのシステムでは「evil.php」として解釈される
// 「.txt」以降は無視される!
なぜこれが危険か?セキュリティチェックの回避例
// セキュリティチェック:.txtファイルだけ許可
$upload_file = $_GET['file']; // ユーザーからの入力
if (strpos($upload_file, '.txt') !== false) {
// .txtが含まれているのでOK
include($upload_file); // ファイルを読み込む
}
攻撃者が evil.php\0.txt を送ると...
$upload_file = "evil.php\0.txt";
// チェック:.txtが含まれている → OK(通過)
if (strpos($upload_file, '.txt') !== false) {
// でも実際にincludeされるのは「evil.php」だけ!
include($upload_file); // evil.phpが実行される!
}
オーバーロングUTF-8とは?
UTF-8には、同じ文字を複数の方法で表現できてしまう「不正な表現」があります。
正常なヌルバイトのUTF-8表現
0x00 (1バイト)
不正な(オーバーロング)UTF-8表現
0xC0 0x80 (2バイト) ← 本来1バイトで表現すべきを2バイトで表現
0xE0 0x80 0x80 (3バイト)
0xF0 0x80 0x80 0x80 (4バイト)
攻撃シナリオ
ステップ1:アプリケーション側のセキュリティチェック
// ヌルバイトチェック(単純な実装)
$filename = $_GET['file'];
// 0x00(ヌルバイト)が含まれていないかチェック
if (strpos($filename, "\0") !== false) {
die("ヌルバイトは禁止です");
}
// .txtで終わっているかチェック
if (substr($filename, -4) === '.txt') {
// OKなのでファイルを開く
$content = file_get_contents($filename);
}
ステップ2:攻撃者がオーバーロングUTF-8を使う
// 攻撃者の入力
// URLエンコードで:evil.php%C0%80.txt
// これは:evil.php[0xC0 0x80].txt という意味
$filename = "evil.php\xC0\x80.txt";
// チェック1:strpos($filename, "\0") → 見つからない!
// なぜなら0x00を探しているが、実際は0xC0 0x80だから
// → チェック通過
// チェック2:substr($filename, -4) === '.txt' → 合致!
// → チェック通過
ステップ3:内部処理でUTF-8デコードされる
// ファイルシステムやライブラリがUTF-8として解釈
// 0xC0 0x80 → 0x00(ヌルバイト)に変換される
// 結果:file_get_contents("evil.php\0.txt")
// → 実際には "evil.php" が読み込まれる!
図解
攻撃の流れ:
1. 攻撃者の入力:
evil.php%C0%80.txt
↓
2. アプリケーションのチェック:
「evil.php[0xC0][0x80].txt」
- ヌルバイト(0x00)はない → ✓通過
- .txtで終わっている → ✓通過
↓
3. 内部処理でUTF-8デコード:
「evil.php[\0].txt」
↓
4. ファイルシステムの処理:
「evil.php」として解釈(\0以降は無視)
↓
5. 結果:
evil.phpが実行される!
現実の例
// 画像アップロード機能
$filename = $_FILES['upload']['name']; // "image.jpg%C0%80.php"
// チェック:拡張子が.jpgか?
if (preg_match('/\.jpg$/', $filename)) {
// OK、アップロード許可
move_uploaded_file($tmp, "/uploads/" . $filename);
}
// 後でアクセスすると...
// /uploads/image.jpg → PHPとして実行される可能性!
対策
// 1. 厳格なUTF-8検証
if (!mb_check_encoding($filename, 'UTF-8')) {
die("不正な文字エンコーディング");
}
// 2. ヌルバイトの複数パターンをチェック
if (preg_match('/\x00|\xC0\x80|\xE0\x80\x80/', $filename)) {
die("ヌルバイトが検出されました");
}
// 3. ホワイトリスト方式(最も安全)
if (!preg_match('/^[a-zA-Z0-9_\-]+\.txt$/', $filename)) {
die("許可されていないファイル名");
}
まとめ
オーバーロングUTF-8攻撃は、「見た目は安全だけど、内部処理で危険な文字に変換される」という二重性を悪用した攻撃です。表面的なチェックだけでは防げないため、文字エンコーディングの厳格な検証が必要になります。
防御方法
1. 文字エンコーディングの検証
// PHPの例
$input = $_POST['data'];
// UTF-8として正しいかチェック
if (!mb_check_encoding($input, 'UTF-8')) {
// エラー処理
die('不正な文字エンコーディングです');
}
// 正しい場合のみ処理を続ける
2. Content-Typeヘッダーの明示
<!-- HTMLのmetaタグで指定 -->
<meta charset="UTF-8">
// PHPのヘッダーで指定
header('Content-Type: text/html; charset=UTF-8');
3. 一貫した文字エンコーディングの使用
- データベース:UTF-8
- アプリケーション内部:UTF-8
- HTTPレスポンス:UTF-8
すべてをUTF-8に統一することで、文字コードの変換ミスによる脆弱性を防ぎます。
まとめ
文字エンコーディングの検証を怠ると、
- XSS攻撃が成功する(UTF-7など)
- SQLインジェクションが成功する(Shift_JISなど)
- ファイルアクセス制限を回避される(オーバーロングUTF-8など)
対策のポイント
- 必ず文字エンコーディングを検証する(
mb_check_encodingなど) - 想定外の文字エンコーディングは拒否する
- アプリケーション全体で文字エンコーディングを統一する(UTF-8推奨)
意外と調べてみたら怖い攻撃だな〜と実感しました!引き続き、セキュリティについてより詳しくなるために頑張ります🔥
Discussion