PHPのheader関数とheaders_remove関数の注意点

(Last Updated On: 2018年8月18日)

PHPのheader関数とheader_remove関数の注意点です。あまり使わないと思いますが、Set-Cookieヘッダーや他のヘッダーで注意しないと問題になります。普段はsetcookie関数でクッキーを設定していてたまたまheader関数でクッキーを設定した、という場合にheader関数とsetcookie関数の仕様の違いにより、思ってもいない結果になります。

header関数の仕様

header関数の仕様は以下の通りです。

void header ( string $string [, bool $replace = true [, int $http_response_code ]] )

header() は、生の HTTP ヘッダを送信するために使用されます。 HTTP ヘッダについての詳細な情報は » HTTP/1.1 仕様 を参照ください。

覚えておいて頂きたいのは、header() 関数は、 通常の HTML タグまたは PHP からの出力にかかわらず、すべての実際の 出力の前にコールする必要があることです。 頻出するエラーとして、include または require 関数、他のファイルをアクセスする関数に 空白または空行があり、header() の前に出力が 行われてしまうというものがあります。同じ問題は、単一の PHP/HTML ファイルを使用している場合でも存在します。

header_remove関数の仕様

header_remove関数の仕様は以下の通りです。
void header_remove ([ string $name ] )

header()関数を使って以前に設定したHTTPヘッダを削除します。

header関数と他のHTTPヘッダー

httpヘッダーは他の関数でも送信されます。例えば、setcookie関数で’Set-Cookie’ヘッダーが送信されます。session_start関数ではセッションIDの’Set-Cookie’ヘッダーやキャッシュ制御ヘッダーが送信されます。

HTTPヘッダーを見るにはphp-cgiバイナリ(CGI用のバイナリ)が便利です。

[yohgaki@dev ~]$ php-cgi
<?php
setcookie('TEST');
?>

X-Powered-By: PHP/5.6.26
Set-Cookie: TEST=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Content-type: text/html; charset=UTF-8
[yohgaki@dev ~]$ php-cgi
<?php
session_start();
?>

X-Powered-By: PHP/5.6.26
Set-Cookie: PHPSESSID=hv2ivf3uelnm4qs9e5bfhqlqi7; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-type: text/html; charset=UTF-8

このような感じでHTTPヘッダーが送信されます。

session_start関数の後にheader関数で’Set-Cookie’ヘッダーを設定してみます。

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
header('Set-Cookie: PHPSESSID=WillThisWork?');
?>

X-Powered-By: PHP/5.6.26
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=WillThisWork?
Content-type: text/html; charset=UTF-8

PHPのセッションIDクッキー名はデフォルトだと”PHPSESSID”です。header関数で’Set-Cookie’を実行すると、それ以前の全ての’Set-Cookie’ヘッダーが削除されます。この為、session_start関数で設定したハズのセッションIDクッキーが消えています

この動作はsession_regenerate_id関数を実行しても同じです。

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
session_regenerate_id();
header('Set-Cookie: PHPSESSID=WillThisWork?');
?>

X-Powered-By: PHP/5.6.26
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=WillThisWork?
Content-type: text/html; charset=UTF-8

先程と同じようにPHPSESSIDが上書きされています。

クッキー名を別の名前に変えるとどうなるか?というと

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
session_regenerate_id();
header('Set-Cookie: FOO=BAR');
?>

X-Powered-By: PHP/5.6.26
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: FOO=BAR
Content-type: text/html; charset=UTF-8

別のクッキー名(FOO)に変えても、PHPSESSIDが消えています

つまり、header関数で’Set-Cookie’ヘッダーを設定すると、Sessionモジュールが設定したセッションIDクッキーも含め、全て消してしまいます

 

header_remove関数と他のHTTPヘッダー

header関数の動作を見て、header_remove関数の動作は予想できると思います。header_remove関数をパラメーター無しで呼ぶと、それ以前のヘッダーを全て削除してしまいます

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
session_regenerate_id();
header_remove();
?>

Content-type: text/html; charset=UTF-8

Content-Typeヘッダーが残っているのは、Content-Typeヘッダーはヘッダーが出力される直前に追加される(つまり、header_remove関数の後に追加される)のでヘッダーが出力されています。

 

header関数とheader_remove関数の注意点

session_start関数やsession_regenerate_id関数の後にheader関数やheader_remove関数を下手につかうと、セッションIDのクッキーやセッション有りのページを保護するキャッシュヘッダーが削除されます。

つまり、セッションIDクッキーが無くなったり、キャッシュさせてはならないページがキャッシュされたり、といったことが起こります

特に、session_regenerate_id関数の後にheader(‘Set-Cookie: something’)とすると

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
session_regenerate_id();
header('Set-Cookie: FOO=BAR');
?>

X-Powered-By: PHP/5.6.26
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: FOO=BAR
Content-type: text/html; charset=UTF-8

のようにセッションIDが削除されるので、セッションが切れて突然ログアウトしたような動作になったりします。

 

まとめ

マニュアルを良く読んで理解すると”当たり前”のことなのですが、こういう細かい事は結構簡単に忘れてしまいます。しかも、実際にこの問題に遭遇するとどこが問題なのか苦労する可能性が高いです。

セッションが無くなったり、キャッシュ不可のページがキャッシュされたり、時間を無駄にしたりしないように頭の片隅に置いておくと良いかも知れません。

因みに、setcookie関数は複数回呼ばれることが想定されていて、以前の’SetCookie’ヘッダーを削除しません

[yohgaki@dev ~]$ php-cgi
<?php
session_start();
session_regenerate_id();
setcookie('TEST', 'VALUE');
?>

X-Powered-By: PHP/5.6.26
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=28iltbc1vbpgeiep93mf857sr1; path=/
Set-Cookie: TEST=VALUE
Content-type: text/html; charset=UTF-8

つまり、setcookie関数を使っていれば他のクッキーヘッダーは保護されます

何らかの理由でsetcookie関数ではなく、header関数を利用し’Set-Cookie’ヘッダーを設定する場合は他の’Set-Cookie’ヘッダーに干渉しないようなコード、つまりheader関数を先に利用するか、header関数のreplaceオプションをFALSEにする必要があります。

replace

オプションのパラメータ replace は、ヘッダが 前に送信された類似のヘッダを置換するか、または、同じ形式の二番目の ヘッダを追加するかどうかを指定します。デフォルトでは、この関数は 置換を行ないますが、二番目の引数に FALSE を指定すると、同じ型の 複数のヘッダを強制的に生成します。例えば、

<?php
header('WWW-Authenticate: Negotiate');
header('WWW-Authenticate: NTLM', false);
?>

header関数を’Set-Cookie’以外に使っている分には「前に送ったヘッダーを削除する」という動作を気にすることはほとんどありません。’Set-Cookie’には注意しましょう!

 

おまけ

HTTPヘッダーの中には複数回設定することが許されている物があります。’Set-Cookie’ヘッダーもその1つです。’Set-Cookie’ヘッダーで同じクッキー名のクッキーを登録することができます。

例えば、こんな感じでTESTクッキーを設定したとします。

[yohgaki@dev ~]$ php-cgi
<?php
setcookie('TEST','A');
setcookie('TEST','B', time()+9999);
setcookie('TEST','C', time()+9999, '/mypath/');
setcookie('TEST','D', 0, '/', 'example.com');
setcookie('TEST','E', 0, '/', 'example.com', TRUE);
setcookie('TEST','F', 0, '/', 'example.com', TRUE, TRUE);
?>

X-Powered-By: PHP/5.6.26
Set-Cookie: TEST=A
Set-Cookie: TEST=B; expires=Wed, 19-Oct-2016 12:03:22 GMT; Max-Age=9999
Set-Cookie: TEST=C; expires=Wed, 19-Oct-2016 12:03:22 GMT; Max-Age=9999; path=/mypath/
Set-Cookie: TEST=D; path=/; domain=example.com
Set-Cookie: TEST=E; path=/; domain=example.com; secure
Set-Cookie: TEST=F; path=/; domain=example.com; secure; httponly
Content-type: text/html; charset=UTF-8

ブラウザは全てのクッキーを受け入れます。つまり、クッキー属性が完全に一致するものは上書きされますが、属性が一致しない物は全て保存されます。

ここで疑問に思う場合があると思います。「どのクッキーが送られてくるんだ?」と思うハズです。

答えから書くと「ブラウザによる」です。クッキーの優先順位は明確にRFCなどで規程されていません。このため、「ブラウザが勝手に一番相応しい」と判断したクッキーを1つ1を送ってきます。

「ブラウザがサーバーに複数のクッキーを送るのは問題だ!」ということで、ブラウザが勝手に決めたクッキー一つだけを送るようになったのですが、これにも問題があります。以前の動作なら複数のクッキーが設定されているか?サーバー側で判別することができました。新しい動作では出来ません。つまり、犯罪者が攻撃目的でおかしなクッキーを設定している場合でも識別できなくなったのです。個人的には「クッキーは全部送ってほしい、こっちで余計なモノを検出し、ユーザーに警告できるから」と思っています。しかし、こうなっているモノはどうしようもありません。

「ブラウザが勝手に一番相応しい」と判断したクッキーが非常に厄介です。「一番相応しい」は属性だけでなく、設定順序も影響します。モダンブラウザは概ね同じように選ぶのですが、古いブラウザには意味不明な選択をするモノもあります。例えば、httponly属性が付いている物より、httponly属性がない物を優先したりしていました。

モダンブラウザだから安心か?というと全く安全ではありません。例えば、貴方のアプリケーションが

http://www.example.com/myapp/

というURLを持っているとして、攻撃者がセッションIDクッキー名と同じクッキーをdomain属性に”example.com”、パス属性に”/myapp/”、httponly属性を有効にしたクッキーをそれぞれ設定した場合、どうなるでしょうか?

PHPのセッションIDクッキーのデフォルトはdomain属性は””(空、つまり今のホスト名)、パスは”/”、httponly属性は無し、です。PHPのセッションIDクッキーでなく、攻撃者が設定したクッキーが使われてしまいます!しかも、普通にセッションIDクッキーを削除しようとしても、属性が一致しないので削除できません。

こういった仕様であることもあり、session.use_strict_mode=Onを設定することが重要です。この設定をすると、セッションモジュールが初期化して、セッションデータベースに存在するセッションIDしか受け入れなくなります。2

例えば、session.use_strict_mode=Offでログインしないでショッピングできるサイトの場合、ユーザーが送ってきたセッションIDクッキーを受け入れてしまうと、そのクッキーは攻撃者が設定した「消せないクッキー」である可能性3があります。会計処理前にセッションIDを更新したなら、攻撃者はユーザーがショッピングカートに入れたモノや氏名、住所などの情報まで盗まれる可能性があります。セッションID更新処理に不備がある場合、攻撃者が設定したセッションIDを使ったままになる可能性もあります。

session.use_strict_mode=Onは確実に安全なセッションID管理には欠かせない設定です。

ヘッダーとクッキーの話だったので「おまけ」として紹介したらおまけレベルにならなくなってしまいました。PHPのセッションモジュールのメンテナンスをしている関係で、この辺りの問題には割と詳しい方だと思うので簡単に一部の問題を紹介しました。

この問題はもう十何年も前から良く知られている問題ですが、直っていません。クッキーはセッションIDに使われる重要なモノだから厳格に仕様が決められていて安全!と思っている方もいると思います。しかし、現実のクッキー仕様はどうしようもないくらい出鱈目です。4


  1. 古いブラウザは「リクエストにマッチするクッキー全て」を送信していました。つまり、Cookieヘッダーに同じ名前のクッキーが複数設定され、返って来ました。しかも、どの条件にマッチして送信されたのか情報が全くない状態で送ってきました。当然、順序もブラウザ毎にバラバラでした。この仕様で困った問題は、リクエストを受け取ったサーバーが複数クッキーがあった場合には「どのクッキーを優先するか?」勝手に決めていました。処理系によっては、最初のクッキーが使われたり、最後のクッキーが使われていました。 
  2. PHPのsession.use_strict_mode=Offの動作は、アダプティブなセッション管理機構と呼ばれる脆弱な動作です。セキュリティ専門家でもsession.use_strict_mode=Onは重要ではない、と言っている人がいるくらい重要性が理解されていません。セキュリティ専門家の場合、致命的な間違いですが、開発者に勘違いするなと言っても専門家に間違った事を言う人がいるくらいです。正しく認識されるまでには時間がかかりそうです。 
  3. 攻撃者はアクティブなクッキーを維持したり、作る必要がありません。永久に有効なクッキーを埋め込んで、利用されるのを待つだけです。session.use_strict_mode=Offの場合、攻撃がより簡単になり、ちょっとした間違いがあると致命的な問題になる場合もあります。 
  4. マトモに仕様と呼べるモノが無く、セキュリティを考慮していなかかったので出鱈目になるのは当然と言えば当然です。クッキーヘッダーに関する仕様はあります。 https://tools.ietf.org/html/rfc6265 クライアントは無法地帯でした/です。 

投稿者: yohgaki