entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

型安全なHTTP入力を保証するParamHelper

こんにちは。pixiv運営本部 開発支援チームでpixivのコーディング環境の向上をしているyosatakです。

入力取得の安全性向上

PHPでは以下のようなURLのクエリパラメータを$_GET['id']経由で取得できます。

https://www.pixiv.net/novel/show.php?id=14921239

しかし、スーパーグローバル変数$_GETは再代入可能な上に、stringだけでなく配列が外部から自由に入力されうるため、取得時に値の範囲を絞り込まなければ以後の処理で想定外の問題が生じる可能性があります。

filter_input()適切なフィルタを用いることで同様の検証が可能です。pixivでは数値の範囲やデフォルト値を設定できるよう何度かの試行錯誤を経て、2016年頃からParamHelperというヘルパクラスを実装することで安全に入力を取得できるようにしています。この仕組みは現在ではPHPStanによる型付けにも大きく寄与しているので紹介します。

ParamHelper

ParamHelperは以下の様に入力の型を検査し、適合しなかった場合例外を出力します

$id = ParamHelper::get('id')
        ->asPositiveInt();

この場合は?id=****の箇所が正の整数であることを検査し、マッチしなかった場合例外を送出します。

また、

$id = ParamHelper::get('id')
        ->withErrorCode(ErrorCode::REQUEST_INVALID)
        ->asPositiveInt();

とすることで、例外の種類を設定し、HTTPのステータスコードを変化させたり、

$id = ParamHelper::get('id')
        ->orDefault(false)
        ->asPositiveInt();
\PHPStan\dumpType($id); // false|int<1,max>

とすることで、positive-int|falseであるとPHPStanで型を付けることができるようになっています。

必須のパラメータが入力されなかったり、文脈に沿わない値が入力された場合は例外が送出され汎用のNotFound画面などが描画されるため、一般的なケースにおいてコントローラ層で個別にハンドリングする処理を省けます。

ParamHelper::get('id')はクエリパラメータから取得する$_GET['id']に対応していますが、同様にParamHelper::post('id')でフォームリクエストから取得する$_POST['id']に、ParamHelper::json('id')でリクエストボディにJSONでエンコードしてリクエストされたペイロードから値を取り出すことができます。

ParamHelperには以下の様な種類のメソッドがあり、様々な入力型の絞りこみができます。

orDefault($default_value)
withErrorCode($error_code)
asArray()
asDateTimeString($datetime_format)
asInt()
asNonNegativeInt()
asNonNegativeIntList()
asPassword():
asPositiveInt(array $values)
asPositiveIntList()
asPositiveIntListFromCommaSeparatedString()
asPositiveIntByExtractingNumbersFromParameter()
asString()
asNonEmptyString()
asStringInArray(array $values)
asStringList()

例えばクエリパラメータsizeで画像サイズ(small, large, デフォルト:medium)を指定する場合

$size = ParamHelper::get('size')
          ->orDefault('medium')
          ->asStringInArray(['small', 'large']);
\PHPStan\dumpType($size); // 'large'|'medium'|'small' 

とすることで?size=(small or large)で未指定やそれ以外の場合はmediumとしてパラメータの種類を絞り込み取得することができます。この型付けの機能は現時点のPHPStanではPHPDocのみでは実現できないためPHPStanプラグインを実装しています。

入力値には厳密に型が付けられているので、コーディング時に値の不必要な検査や、検査漏れについて警告を出すことができます。入力値に対してコントローラで明示的にキャストする必要もないため、型宣言された関数呼び出しの引数として利用する際にも考慮漏れによって誤ったキャストで入力値を破壊するリスクも排除できるのが大きなメリットです。

asPassword()はパスワードのような機微情報の入力を取り扱うPasswordクラスのインスタンスとして値を返します。Passwordクラスはパスワードの検証やハッシュ化などの操作を内包しており、万が一にもバックトレースやエラーログなどに平文のパスワードが記録されないように工夫しています。

Webアプリケーションの入力の扱いには細心の注意が必要ですが、pixivではこのクラスを利用することで入力を安全に扱いやすくしています。ともすればコピペになりがちなコントローラでの入力値を受け取る処理の繰り返しが簡潔なメソッド呼び出しになることで、イージーミスによるバグの入り込む余地も減らすことができます。

pixivでは実装上の割り切りとしてスーパーグローバル変数に直接依存していますが、関連プロダクトのピクシブ百科事典はPSR-7とPSR-15に依存しているため、そのままコピペで持ち込むと問題があります。その問題を解決するためにParamHelperを発展させた仕組みが導入されており、そちらについての記事も近日中に公開予定です。

PHP Conference Japan 2021が開催されます

約一ヶ月後の10月2日から3日にかけてPHP Conference Japan 2021が開催されます。

ピクシブからは@tadsanが10月3日の10:00から「配列、ジェネリクス、PHPで書けない型」と題して、PHPに詳細に型をつけるための静的解析ツールに実装された発展的な型とジェネリクスについて話します。

fortee.jp

また、この記事に関連して@yosatakが「PHP 静的解析 活用術 2021」を@tadsanの発表の直後の10月3日11:20から発表します。pixivで利用されている最新のPHPStan活用術やPHPStan拡張の作りかたを共有する予定ですので、手元のPHPアプリケーションの改善に役立つ内容となると思います。

fortee.jp

今年のPHP Conference Japanはオンライン開催となりますので、普段東京に来にくいPHPerも参加しやすくなっています。 この上で紹介したセッション以外にも、PHPerの方々なら興味を引くセッションが盛り沢山です。

phpcon.connpass.com

参加登録は無料です! 是非ご参加ください

icon
yosatak
開発支援チームでpixivのコードを型解析しています.PHP,Elixir,POSIX Shell Scriptが好きです.気軽に出国できていた頃は海外旅行が趣味でした