一応、PHPの危い関数リスト、は存在します。例えば、以下のような物があります。
後者は主にホスティング環境(?)でリスクがある関数の一覧を作るプログラムのようです。
ファイルを操作する関数、コマンドやスクリプトを実行する関数などのリスクは自明だと思います。少し趣向を変え、間違えて使うと危い関数の一覧を独断と偏見で作ってみました。
【重要】こういった「危険な物」を定義するのはブラックリストです。ブラックリストは仕組的に危険です。ブラックリストに頼るのは脆弱性を作るような物です。ブラックリスト”だけ”で安心しないでください。最後にどうすれば良いのか?を書きます。
hash_hkdf()
hash_hkdf()は秘密鍵から実際に利用する鍵を導出する関数です。鍵を作る関数なので使い方を間違えると非常に危険です。しかし、hash_hkdf()のシグニチャは本当に危いです。わざわざ危い使い方をするように作ってあります。
string hash_hkdf ( string
$algo
, string$ikm
[, int$length
= 0 [, string$info
= ” [, string$salt
= ” ]]] )
最後のオプション引数ですが、$saltは秘密鍵($ikm)の安全性を確保する最も重要な引数です。
必須の$salt、HKDFを使うなら必須といえる$infoが最後の方の”オプション”引数になっています。hash_hkdf()の引数順序はデタラメで、最も重要な引数が最後、次に重要な引数のその前にあります。必ず両方利用します。
参考:
uniqid()
そもそもuniqid()はメールメッセージ用のIDとして作られた物のはずです。
string uniqid ([ string
$prefix
= “” [, bool$more_entropy
=FALSE
]] )
デフォルトでは現在時間の浮動小数点データをHEX化した文字列を返します。つまり、uniqid()は単なるタイムスタンプです。
$more_entropy=TRUEとして追加のエントロピー付きで取得しても、大してランダムな値を生成しません。$more_entropy=TRUEで生成する疑似ランダム値は”時間ベース”のcombined LCGで生成されています。
安全なランダム値が必要な場合はrandom_bytes()かrandom_int()を利用します。
uniqid()の名前から推測されるような安全なランダム値を生成する物ではありません。誤用は非常に多いです。
実は古いPHPのセッションIDも同じような感じで作られていました。さっさと直そう、と提案してもなかなか実現しませんでした。
CSPRNGを使うように実装された後でも、ほぼあり得ないですがCSPRNGからランダムデータを取得できない際には時間ベースランダムにフォールバックする仕様になっており、これは私が削除しました。
きちんと動かない物はきちんと失敗させる方が良いです。
htmlspecialchars()、htmlentities()
htmlspecialchars()、htmlentities()は何も考えないで使うと危険な関数です。
string htmlspecialchars ( string
$string
[, int$flags
= ENT_COMPAT | ENT_HTML401 [, string$encoding
= ini_get(“default_charset”) [, bool$double_encode
=TRUE
]]] )
不十分なエンティティ化
$flagsオプション引数でHTMLエンティティ化する文字を指定しますが、ENT_COMPATは ‘ (シングルクオート) をエンティティ化しません。
$tag = “<tag attr='”. htmlspecialchars($var) .”‘>”;
分かりづらいですが上記のコードは属性をシングルクオートで囲んでいます。この場合、 ‘ (シングルクオート) がエンティティ化されないので、エスケープの意味がありません。安全性を考えるならENT_QUOTESを利用します。
これらのHTMLエスケープ関数は / (スラッシュ)も” “(半角スペース)もエスケープしません。HTML5の仕様ではクオートしないタグ属性値を許可していますが、PHPのHTMLエスケープ関数はクオート無しの属性値を安全に処理できません。(これはPHPに限らないです)
文字エンコーディングバリデーションによるDoS
古いはPHPの場合、デフォルト文字エンコーディングがISO-8859-1だったので、デフォルトでは文字エンコーディングのバリデーションは実質的に無効の状態でした。
PHP 5.6からデフォルトの$encodeingはUTF-8になっています。つまり$stringの文字列はUTF-8としてバリデーションされます。バリデーションされるなら問題ないのでは?と思うかも知れません。しかし、出力時のバリデーションは遅過ぎます。
htmlspecialchars()、htmlentities()は不正な文字エンコーディングデータを見つけると、空文字列を返します。不正な部分を除去しようとするブラックリスト対策は脆弱な対策であり、行ってはならないので動作としては正しいです。
しかし、出力時の文字エンコーディング バリデーションは遅すぎるのでページの一部が欠ける、といったDoS攻撃に脆弱になります。このようなアプリケーションは山のようにあります。
このような脆弱性を作らない為には、できる限り早く(Fail Fast原則)入力データを”全て”バリデーションしなければなりません。
参考:
rand(), mt_rand()
rand()はPHP 7.1から内部的にはmt_rand()と同じに変更されました。以前のrand()は非常に脆弱(予測しやすい)な疑似乱数を生成していました。特にWindows環境ではとても脆弱でした。
予測可能な疑似乱数であること以外の脆弱性にPHPのmt_rand()には仕様的な欠陥があります。
- mt_rand()の初期状態はたった2^32-1しかない。本来は2^19937-1
- Reseed無しにMT Randバッファーを使い続ける。
- MT Randバッファーはリクエストを跨いで使われる。
誰でも簡単に攻撃できる、とまでは言えませんが本気になれば攻撃可能(生成される疑似乱数を高精度で予測可能)です。
そもそもMT randの疑似乱数は”予測可能”な乱数なので致命的な問題とまでは言えないかも知れません。しかし、本来あるべき強さに比べ、PHPのmt_rand()は強さは塵の質量と全宇宙の質量以上の違いがあります。(塵を1gとして計算すると本当にそうなります。天の川銀河の質量は太陽の約7000億倍くらい、銀河系の数は2兆個ほどと言われています)
本来の強さであれば、本気になっても予測することはかなり困難です。
※ 最も短いパッチなら1行パッチで修正できるのですが、この意味を理解できない人も居るので直ていません。
プリペーアドクエリ全部
プリペアードクエリ全部が危険!?そんな馬鹿な!と思うかも知れません。しかし、プリペアードクエリの過信、が現在のNo. 1のSQLインジェクション脆弱性の原因です。
現在ではプリペアードクエリは危険な関数の筆頭と言っても構わない状況です。コード検査で見付かるのはコレがほとんどです。
SQLインジェクションは対策が行いやすく、比較的容易にほぼ完全に防止できます。もしSQLインジェクション脆弱性があった場合のダメージ(データ保護的な意味のみでなく、開発者へのダメージも含みます)はかなり大きいです。「プリペアードクエリを使っていれば安全」との過信から間違いが生まれています。
プリペアードクエリはSQLクエリの引数(パラメーター)によってSQLインジェクション攻撃ができないようにします。言い換えるとSQLパラメーター”しか”保護でないのがプリペアードクエリです。
プリペアードクエリでは”識別子”のセキュリティ対策ができません。特定のカラムを抽出、ソートしたりするプログラムは一般的です。現在では識別子のエスケープ漏れが第一位のSQLインジェクション脆弱性です。
プリペアードクエリはSQLパラメーターとしての引数は保護しますが、その先のコンテクストには無力です。現在のRDBMSはXML、JSON、正規表現、配列といったSQL以外の機能を持っています。プリペアードクエリでは”これらの機能”のセキュリティ対策ができません。
詳しくは「完全なSQLインジェクション対策」を参照ください。
urlencode(), rawurlencode()
urlencode(), rawurlencode()のように単純な関数が危険?!と思うかも知れません。結構危険です。なぜなら少なくない開発者が”識別子”(パラメーター名)のエスケープを忘れるからです。
”識別子”がコードに書かれたリテラル(変数でなく、コードに直接値が書かれた値)であれば問題無いのですが、変数になっている場合も少なからずあります。
配列からクエリ文字列を生成する場合、識別子と値の両方エスケープするhttp_build_query()を利用した方が安全です。
escapeshellcmd(), escapeshellarg()
escapeshellcmd()はプログラマがコマンドを記述し易くするためのヘルパー関数です。escapeshellcmd()はセキュリティ対策に使えません。
escapeshellarg()はコマンドライン引数がパラメーターであっても出来るだけ安全に出力できるように助ける為の関数です。セキュリティ対策としてescapeshellarg()に頼ってはいけません。コマンド実行関数に渡す文字列は全て完全に安全に渡せる文字列であること、escapeshellarg()に渡す文字列はescapeshellarg()が完全に安全に処理可能な文字列であること、をバリデーションしなければなりません。
HTMLエスケープ関数でバリデーションが行われると困った問題(DoSなど)が起きてしまう原理と同じで、出力時でのバリデーション”だけ”に頼ると問題になります。
HTMLエスケープで問題を避けることと同じです。できる限り早く(Fail Fast原則)入力データを”全て”バリデーションしなければなりません。
参考:
pcntl_exec()
コマンド実行系の関数は全て危険ですが、pcntl_exec()なら大丈夫!と誤解しているとコマンド実行脆弱性を作ってしまいます。
pcntl_exec()はsystem()などと異り、コマンドとコマンド引数を分離した形でコマンドを実行でき、system()などコマンドと引数を一緒に記述するコマンド実行系に比べると、比較的安全にコマンドを実行できます。しかし、あくまでも”比較的”安全に実行できるだけで危険です。
コマンドと引数が分離できているから安全、ではありません。コマンド自体が変数の場合、別のコマンドを実行されてしまいます。これはSQLのプリペアードクエリを使っていても”識別子”によるインジェクションが可能になることに似ています。
pcntl_exec()でシェルスクリプトを実行しても、実行したシェルスクリプトがコマンド引数による不正コマンド実行を考慮しない場合、不正なコマンドを実行可能です。
結局のところ、何を使っていてもコマンド実行系の関数を安全に実行するには、変数の内容が安全であることを確実にバリデーションするしかありません。pcntl_exec()を過信していると落とし穴に落ちてしまいます。
参考:
int型
データ型なので”関数”ではありませんが、危いので取り上げます。PHPのint型のサイズはアーキテクチャー依存です。つまり、32bit CPUなら符号付き32bit整数、64bit整数なら符号付き64bit整数です。1
「とは言っても32bit CPUでもそれ以上の整数が正しく取り扱えていたけど?」と思うかも知れません。それはPHPが自動的にfloat型に変換しているからです。float型の場合、符号付き53bit整数まで正しく扱えます。
符号付き53bit整数まで扱えるなら問題なしでは?!と思うかも知れません。しかし、
$safe_int = (int)$string_int;
などとキャストすると”強制的にint型”になります。そしてオーバーフロー/アンダーフローのエラーは起きません。つまり、オーバーフロー/アンダーフローした出鱈目な整数が保存されます。
「データベースのIDカラムは64bit整数型なのでint型にキャスト」といった操作をすると。。。ある日突然、データベースが動かなくなります。このような問題はデータベースに限らず起きる可能性があります。
データベースやJSONデータを取り扱っている場合、不用意な整数型キャストは危険です。こういったデータはバリデーションで”整数として取扱える文字列”としてバリデーションし、”文字列型”のまま利用します。
データベースやJSON、XMLなどはそもそもテキストデータでデータをやり取りするインターフェースです。本来はテキストで問題ないのですが、JSONやXML、特にJSONでは問題になります。json_encode()はint型かfloat型でないと数値としてエンコードしてくれません。インターネットRFCではこの点にも言及していて、正しく取扱える整数は符号付き53bit整数までと考えるべき、としています。
整数や数値だから大丈夫!!ではありません。信頼性が高いシステムを作るには整数や数値にも注意してプログラムを作る必要があります。
参考:
json_encode()
json_encode()はデフォルトのままでは安全な形の出力を行いません。理想的にはASCII文字も含めたUnicodeエスケープ形式でエスケープするのが一番です。しかし、この機能はありません。
Unicodeエスケープ形式でエスケープできない為、Unicodeエスケープを用いた攻撃に対して脆弱になっています。
バリデーションしていても、Unicodeエスケープでバリデーションを回避し、意図しない文字列をインジェクション可能です。ただし、これはバリデーション方法の問題でもあります。
バリデーションのベストプラクティスは「バリデーション前に必要なデコードや正規化などの処理を行ってから行う」です。
参考:
json_decode()
前のint型問題にも出ているjson_decode()には既に記述した「int型」問題があります。IoTデバイスなどでは32bit CPUも少なくありません。こういったデバイスにPHPを入れてJSONを使う場合に注意が必要です。32bit CPUだと大きな値がfloat型になってしまいます。
スカラータイプヒントを使うと、動かなくなったりします。何も考えずに整数は符号付き64bit整数、を前提条件にしていると問題を作ってしまいます。
こういったデータ型の違いによる問題はPDOにもあります。
データ種別の考慮しないデータ型の自動変換はやってはならないアンチプラクティスです。自分でライブラリを作る場合にも注意が必要です。
正規表現関数全部
正規表現は複雑な文字列検索や置換を可能にする便利な機能です。しかし、無限再帰や半無限再帰が可能といった問題があります。
PCREには一応対策(ネストレベル、バックリファレンス制限)があるのですが、mbstringの正規表現には関数ネストレベルの調整さえありません。(Onigurumaにはネストレベル制限が追加されているのですが、まだ誰もパッチを書いていません。ネストレベルの設定変数がスレッドセーフでないことも理由の1つです)
ユーザー入力に正規表現を許可する場合、かなり限定的にして許可しないと危険です。基本、ユーザー入力は許可すべきでないです。
何も考えないで正規表現を書いても危険です。
参考:
Session
関数でなくモジュールです。しかも、Webアプリセキュリティの要のSessionモジュールです。基本、PHPのSessionモジュールは設計として脆弱です。
設計として脆弱、とはそもそも持っているべき機能がない、という事です。
しっかりしたセッション管理にはセッションデータにタイムスタンプを付与し、有効期限管理をきっちり実施する必要があります。PHPのSessionモジュールにはこの機能がありません。
完璧に動作するパッチまで作って提案したのですが、Sessionセキュリティに欠かせない要素である、という事が理解できない人が多く居たためマージできていません。(最初にPHPプロジェクトに提案した半年後(?)くらいにOWASPのセッションセキュリティガイドも同じ内容に更新されました)
ユーザーレベルでも有効期限管理は出来るので、自分で管理してください。管理し易いようにPHP 7.0のsession_regenerate_id()は現在のセッションデータをセーブし、新しくセッションを作るよう仕様変更しました。PHP 7.0以降なら簡単に実装できます。
とは言っても、アプリコードに変更を加えたくない、というケースも多いです。そういう時に為に、ユーザー定義(PHPで書いたコード)でセッションデータをシリアライズできる様にする提案もしました。
有効期限管理なんて要らない、というセキュリティとは何か、理解していない人が少なからず居たため、これもマージされていません。
残念ですが、自分で実装する、自分でアプリを変更する、という形で対応しなければなりません。
参考:
この他に、セッションデータ管理がアダプティブである問題もあります。これに関してはパッチはマージされています。しかし、デフォルトでは有効化されていません。
参考:
SQLite
これはSQLiteの仕様の問題でPHPの問題ではありませんが、知っておかないと危険な仕様です。SQLiteはPDOで利用できます。使っている方も少なくないでしょう。
SQLite3のカラムは例外(プライマリキーの整数型)を除き、中身は全て文字列型です。カラムデータ型にINTやDATEを指定しても、文字列なら何でも保存できます。これがSQLite3の仕様です。
プリペアードクエリを利用していても、何の保護にもなりません。問題なくアプリを動作させるにはデータバリデーションが欠かせません。
データバリデーションの甘いアプリの場合、不正なデータが保存され、あらゆる問題が発生する可能性があります。対策はデータバリデーションを確実に行う、です。
参考:
mail()
mail()には複数の問題があります。mb_send_mail()はmail()のラッパー関数として実装されているので同じ問題があります。
bool mail ( string
$to
, string$subject
, string$message
[, mixed$additional_headers
[, string$additional_parameters
]] )
$toと$subjectにはメールヘッダーインジェクション対策があるのですが、$additional_headersには対策がありませんでした。私が直したのですが、バグフィックスとして直したので詳しいバージョンを覚えていません。多分、5.6/7.0の何れかです。
今の$additional_headersには不完全なメールヘッダーインジェクション対策が実装されています。(時間を取れていないので直しきれていません)メールヘッダー分割によるメッセージの完全書き換えは防止できているのですが、余計なメールヘッダーのインジェクションは今でも防止できません。
$additional_parametersはUNIX環境ではsendmailコマンドへのパラメーターになります。$additional_parametersは内部的には既に紹介した危険な関数であるescapeshellcmd()と同じ仕組みでエスケープされています。エスケープしているから大丈夫では?と思うかも知れません。しかし、escapeshellcmd()はヘルパー関数です。セキュリティ的なエスケープとしては意味がありません。
$additional_headersも$additional_parametersもバリデーションし、安全なデータにしてから渡さないと問題の原因になります。余計な宛先にメールを送られたり、不正にOSコマンドを実行されます。
問題になることはあまりないと思いますが、mail()は最低限の引数チェックしか行いません。つまり、メールヘッダーインジェクションに利用される改行チェック以外、メールシステムが誤作動するかも知れないRFCを完全に無視したバイナリなどでもそのまま送ります。(こういう仕様はPHPに限りません。mail()にも限りません)
予想外のトラブルを未然に防ぐにはmail()に渡すデータはバリデーション済みである必要があります。
imap_open()
imap_open()はリモートシェルの実行が可能です。PHP 5.6にはデフォルトで無効化できるようにINI設定が追加されました。
接続サーバーを任意に設定できる場合、バリデーションをしっかりやらないと任意コマンド実行が可能になります。※ 参考 どんな入力もですが、バリデーションせずにパラメーターを渡すのは自殺行為です。
serialize()/unserialize()
unserialize()は信頼できない入力データに対して利用してはなりません。これは仕様です。Ruby/Pythonも同様に”仕様として”シリアル化したデータのアンシリアライズの安全性は保証していません。
入力データが信頼できる(改ざんされていない)ことを確認するにはhash_hmac()やhash_hkdf()を利用し、バリデーションします。
存在しないセキュリティ対策用の関数
セキュリティ対策用の関数が存在しないのはとても危険なことです。しかし、無い物は仕方ありません。”無い”ことを認識して、安全に実行できるコードを書くしかありません。
プリペアードクエリの部分で紹介したSQL”識別子”にはエスケープが必要です。識別子エスケープ関数があるのはpgsql(PostgreSQL)モジュールだけです。
RDBMSの機能ですが、LIKEクエリもエスケープが必要です。LIKEクエリ用のエスケープ関数はRDBMSにもPHPにもありません。
XPathにもエスケープ関数がありません。PHPで利用可能なXPath 1.0に至っては”エスケープ仕様”さえありません。XPath 1.0の仕様は仕様として壊れています。(XPath 2.0には”エスケープ仕様”は定義されている)
正規表現用のエスケープ関数はPCRE用はありますが、mbstring用はありません。
JavaScript文字列、CSS用のエスケープ関数もありません。これらは自分で作るかライブラリを利用しなければなりません。
まとめ
何も考えないで使っても◯◯関数が全て問題を完全に解決/解消してくれる!!という物はほとんどありません。
これはセキュリティ対策が「全体」として機能するように設計されていないと機能しないからです。遅すぎる段階でデータをバリデーションしても、エスケープしても、安全だと言われているプリペアードクエリのようなAPIを利用しても、対策の実施の時期が遅すぎて問題の原因になります。これら「個別」対策だけでは何時まで経っても満足できるセキュリティレベルになりません。(達成すべきレベルによりますが)
まだまだ色々あります。時間がある時に追記したいと思います。
このリストはブラックリストです。Railsセキュリティガイドに良いことが書いてあります。
ブラックリストではなくホワイトリストに基づいた入力フィルタを実施することが絶対重要です。ホワイトリストフィルタでは特定の値のみが許可され、それ以外の値はすべて拒否されます。ブラックリストを元にしている限り、必ず将来漏れが生じます。
ブラックリスト対策は構造的にダメな対策です。このリストも信用しないでください。全てのリスクを書き尽すつもは初めから無いですし、可能ではありません。
結局どうすれば良いのか?
正しく(安全に)動作するプログラムには
- 正しいコード
- 正しい/妥当なデータ
の両方が必要です。どちらが欠けてもプログラムは正しく動作しません。
コードを正しく/問題を起こさない書き方で書くことも重要です。同じく「全て正しい/妥当なデータであること」を保証することが重要です。しかし、ほとんどのアプリケーションでデータの妥当性検証が不十分な状態です。プログラムで利用されるデータであっても、全く検証されていないデータも沢山あるのが現状です。
コードを正しく/問題を起こさない書き方で書く、はセキュアコーディング標準を構築することで対応できます。
「全て正しい/妥当なデータであること」を保証する、は入力バリデーションで保証できます。
コンピュータプログラムは「正しい/妥当なデータ」でしか、正しく動作できません。ここで紹介した問題の多くは、データの妥当性検証がないことが原因です。
プログラムには正しく処理できないデータは出来る限り早く廃除することが、全体対策として必須です。これはセキュアコーディング原則の第1原則です。
次に、出力する際にコンテクストに合わせて”完全に無害化”することが欠かせません。これはセキュアコーディングの第7原則です。
これら2つの原則だけでは勿論不十分です。リスクに包括的/全体的/個別的に対応することが必要です。一つだけユニバーサルに利用可能な原則があります。それはゼロトラスト(何も信頼/信用しない)です。安全であると検証/保証されない限り信頼しない原則です。検証/保証されたモノでさえ信頼せず、定期的にレビューします。
参考:
- Windowsだけは特別でPHP 5.6までCPUに関わらず符号付き32bit整数でした。 ↩