404ページでCSRFトークンをセットすると稀に環境依存のバグが起こるかもしれない
ことの発端
あらゆるページにログインフォームがあるサイトの開発環境をWindows上のApacheからWSL2のDockerに移行したところ、なぜかログインができない。。さらにログインできないのはChromeだけでFirefoxやEdgeだと問題がない。。。なぜ?
よくよく調べるとChromeの場合、CSRF対策のために発行していたトークン文字列がなぜか書き換えられていることが判明しました。そこから四苦八苦しながら調査を行い、一応の原因と対処法を突き止めることができたのでここに記録しておきます。
問題を再現する最小限の構成
ブラウザ
Google Chrome 85
サーバー環境
- Docker Desktop for Windows
- WSL2 (Ubuntu20.04)
- php:php7.4-apache イメージ
ソースファイル
ドキュメントルート
├─ .htaccess
├─ 404.php
└─ index.php
ErrorDocument 404 /404.php
<?php
session_start();
$_SESSION['token'] = '404 Not Found!';
<?php session_start(); ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<?php
if(isset($_POST['token']) && isset($_SESSION['token'])){
echo "POST: {$_POST['token']} <br>";
echo "SESSION: {$_SESSION['token']} <br>";
}
$_SESSION['token'] = rtrim(base64_encode(openssl_random_pseudo_bytes(32)),'=');
?>
<form name="testform" method="post" action="">
token: <input type="text" name="token" value="<?php echo $_SESSION['token']; ?>"> <br>
<button type="submit">テスト</button>
</form>
</body>
</html>
index.php
にアクセスしてテストボタンを押すと、送信されたトークンとセッション変数に格納されたトークンが表示されます。本来ならば一致しているはずのそれぞれの値が、なぜかセッション変数のほうが404 Not Found!
の文字列になることがあります。
発生する問題の特性
環境に依存する
- ブラウザ
- デスクトップのEdge、Firefoxを試しましたが発生しませんでした1
- サーバー環境
- 移行前の開発環境ではChromeでも発生しませんでした
- 本番環境(非Docker環境)でももちろん発生していません
送信間隔が短いと発生しない
index.php
のテストボタンを連打するような場合(実際のサイトならありえない速度で必要情報を入力して送信するようなケース)では起きないことが多かったです。そこで、下記のコードを利用して送信間隔と問題発生の相関を大まかに調べる実験を行いました。
<?php
session_start();
$interval = 1200; // ms
$trial = 50; // 試行回数
$remaining = $trial;
if(isset($_POST['remaining'])){
$remaining = intval($_POST['remaining']) - 1;
}else{
$_SESSION['wrong_sessions'] = 0;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<?php
if(isset($_POST['token']) && isset($_SESSION['token'])){
echo "POST: {$_POST['token']} <br>";
echo "SESSION: {$_SESSION['token']} <br>";
if($_POST['token'] != $_SESSION['token']){
$_SESSION['wrong_sessions']++;
}
}
$_SESSION['token'] = rtrim(base64_encode(openssl_random_pseudo_bytes(32)),'=');
?>
<form name="testform" method="post" action="">
token: <input type="text" name="token" value="<?php echo $_SESSION['token']; ?>"> <br>
remaining: <input type="text" name="remaining" value="<?php echo $remaining; ?>"> <br>
<button type="submit">テスト</button>
</form>
<button type="button" onClick="location.href=''">再試行</button>
<?php if($remaining > 0): ?>
<script>
setTimeout(function(){
document.forms.testform.submit();
}, <?php echo $interval; ?>);
</script>
<?php else:
// interval, trial, wrong
file_put_contents('result.csv', "$interval, $trial, {$_SESSION['wrong_sessions']}\n", FILE_APPEND);
endif; ?>
</body>
</html>
結果は次のグラフに示す通りです。各インターバルそれぞれにつき50回ずつ試行し、セッション変数の書き換えが発生した割合を示しています。1000ms前後で発生割合が著しく変化していることが分かりました。
セットされていないセッション変数が新たに追加されることはない
セッション変数の書き換えは行われますが、新規追加は行われません。例えば、404.php
のセッション変数のインデックスを'token'以外にしたとしても追加はされません。
原因と対処法
原因
今回の問題は、PHP、google chromeでだけ不具合(ErrorDocument 404が原因)と類似していることから、404エラーページ周辺に絞って原因究明を進めました。(実際にはindex.php
と404.php
で同じファイルをインクルードする等していたため、ここにたどり着くまでにかなり難儀しました。)
最終的に、原因はChromeが存在しないsw.js2を暗黙的に読み込もうとしているからでした。つまり、問題発生までは
-
index.php
を読み込む(セッション変数をセット) - Chromeがsw.jsを読み込もうとするが存在しない
- 404エラーが発生し、エラードキュメントの
404.php
が返される(セッション変数が上書きされる) - フォームの値を送信すると、セッション変数が
404.php
で書き換えられたもののためトークンが一致しない
というような流れになっていると推測できます。試しに、404.php
で$_SESSION['token'] = $_SERVER['REQUEST_URI'];
とすると、セッション変数が/sw.js
に書き換えられることからsw.jsがリクエストされていることが分かります。
送信間隔が短いと発生しないのはsw.jsが非同期的に読み込まれているからでしょうか?詳しくないのであくまで憶測でしかないですが。
環境に依存することとセットされていないセッション変数が新たに追加されることはないの理由は全く分かっていません。サーバーサイドの環境依存性についてはChromeのネットワークログ(chrome://net-export/)を利用して本番と同様の環境でのリクエストを解析しましたが、sw.jsのリクエストはありませんでした。
対処法
対処法として
- sw.jsをドキュメントルートに設置する
-
.htaccess
にErrorDocument 404...
の記述をしない -
.htaccess
でsw.jsへのアクセスを拒否する
のいずれかを行うことは効果がありました。.htaccess
でsw.jsへのアクセスを拒否する場合は次のような記述です。この場合はErrorDocumentの指定はあっても問題ありません。
<Files ~ "^sw\.js$">
Require all denied
</Files>
まとめ
- Chromeは環境によってはsw.jsを暗黙的にリクエストすることがある
- 存在しないsw.jsへのリクエストが発生した場合に、404エラードキュメントでセッション変数をセットしていると意図しない上書きが発生する
- sw.jsを存在させるか、アクセスを拒否するなどで対策が可能
今回はChromeがsw.jsを暗黙的に読み込むために問題が発生しましたが、読み込む内部リソースが1つでも404エラーになるようなページではこの問題が起こり得ます。したがって、そもそも404ページで正常系の動作に影響を及ぼすセッション変数をセットすべきではないのかもしれません。
-
「発生しない」とは書きましたが、sw.jsと同様の理由でfavicon.ico(アイコン画像)を探しに行く場合は問題が発生しました。しかし、この場合は一度発生した後はブラウザを再起動しない限り再発しませんでした。 ↩
-
sw.jsはService Worker関連のJavaScriptファイルです(参考:https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja) ↩
Author And Source
この問題について(404ページでCSRFトークンをセットすると稀に環境依存のバグが起こるかもしれない), 我々は、より多くの情報をここで見つけました https://qiita.com/goemontech/items/1986b564f1a82fe57c57著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .