Perl Hackers Hub

第40回 Perl開発への動的な型制約の導入(3)

この記事を読むのに必要な時間:およそ 4 分

型制約で不正なリクエストパラメータを制限する

Webアプリケーションを作っていると,悪意のあるユーザーが不正なリクエストパラメータを送ってくる場合があります。不正なものをそのまま受け入れるとアプリケーションに不具合を起こすこともあるため,チェックが必要です。

たとえばブログの状態を公開publicにするか,非公開privateにするかをユーザーがWeb上で選べ,それがHTMLのセレクトボックスで実装されているとします。

<select name="blog_status">
  <option value="public">公開</option>
  <option value="private">非公開</option>
</select>

このとき,普通に利用しているユーザーからは,blog_statusとしてpublicprivateのどちらかしか送られません。しかし悪意のあるユーザーの場合,別の文字列を送ってくる可能性があります。

このような悪意のあるパラメータをいちいちチェックするのは大変です。チェックをする方法はいろいろありますが,型制約を応用して簡単にチェックすることもできます。

リスト4は,HTTPリクエストを処理するモジュールのPlack::Requestを継承し,typed_paramというメソッドを定義したものです。typed_paramメソッドは,パラメータ名と型名を渡せば,型制約を満たすならその値を,そうでなければundefを返します。

リスト4 typed_paramの実装

package My::Request;
use parent qw(Plack::Request);
use Mouse::Util::TypeConstraints;

sub typed_param {
    my ($self, $key, $type) = @_;

    # リクエストパラメータがなければundefを返す
    my $val = $self->parameters->get($key) // return undef;

    # パラメータが制約を満たすならそのパラメータを,
    # 満たさないならundefを返す
    return find_type_constraint($type)->check($val) ?
        $val : undef;
}

このtyped_paramとリスト3で定義したBlog::Status型を利用すれば,普通のユーザーの利用なら正しくpublicかprivateの値を受け取り,悪意のあるユーザーがevilのような値を送ってきたらundefを受け取れます。undefが返ってきている場合は不正なリクエストであると判断できるため,エラー処理を単純化できます。typed_paramメソッドの利用例は次のとおりです。

my $req = My::Request->new($env);

my $status = $req->typed_param(     
    'blog_status', 'Blog::Status',  ┣(1)
);                                  
if (! defined $status) {
    # Bad Requestなどを返す
}

(1)のように,受け取りたいリクエストパラメータ名blog_statusを第1引数に,Blog::Statusを第2引数に渡します。Blog::Status型がpublicprivateしか受け付けないため,悪意のあるユーザーがevilという値を送信してきたら,変数$statusにはundefが代入されます。undefの場合にBad Requestを返すなどの処理をすれば,不正なパラメータを防げます。

動的な型制約を導入してみて

筆者は,所属するはてなの最近のプロジェクトで今回紹介した動的な型制約を導入し,半年間ほど運用しました。運用してみると,だんだん導入のメリットやデメリットが見えてきました。そこで本節では,筆者が感じたメリットやデメリットを紹介します。

メリット

動的な型制約の導入によって感じたメリットは以下の3つです。

関数の使い方の間違いで不整合が起こらない

最初に紹介したとおり,関数を間違って使っても,それだけではデータの不整合が起こらないことは大きなメリットです。

長くアプリケーションを運用していると,関数を定義する人と関数を呼び出す人は別であることも多くなります。自分が定義した関数でも,長く触っていない部分であれば,どんな関数であったか忘れてしまいます。結果として,呼び出し方を間違えることは頻繁に起こります。

引数に型制約をかければ,呼び出しの間違いで関数が中途半端に実行されることはなく,呼び出し間違いによるデータの不整合は起こらなくなりました。

関数に何を渡せばよいかが明確になる

関数の引数に何を渡せばよいかわかりやすくなるメリットも感じました。

動的な型制約を導入していない状態では,引数にどんなオブジェクトを渡すのか,どのパラメータが必須なのかをPerlのシンタックスから判断できません。そのため引数の変数名か,引数に何を渡すか書かれたコメントから判断するしかありませんでした。

Smart::Argsで型制約をかけておけば,Smart::Argsのシンタックスによって渡すべき型が明示され,何を渡せばよいかすぐに判断できるようになりました。

誰が書いても厳しくチェックされる

Smart::Argsを導入する前も,必須の引数がない場合は例外を出し,関数が中途半端に実行されないようにする対策は行っていました。しかし,このやり方は人によってどこまでチェックするかがまちまちで,場合によってはまったくチェックしていないこともありました。

Smart::Argsは,簡単な記法で引数の型が正しいことのチェックを行えます。そのため,プロジェクトメンバーの誰もが型制約を書くようになり,関数の呼び出し間違いのチェックが必ず行われるようになりました。

デメリット

型制約を導入するデメリットはほとんどありませんでした。1つだけ挙げるとすると,呼び出しの回数が多い場合に遅くなることです。

呼び出しの回数が多いと遅くなる

静的言語での型チェックはコンパイル時に行われ,型チェックで実行が遅くなることはありません。しかし動的な型チェックはプログラムの実行時に行われるため,関数を実行するたびにチェックも実行されます。

Smart::ArgsやMouseの型制約のしくみは,実行時に動作することに配慮して高速に動作します。それでも数万回も呼び出すと,体感できるほどに実行が遅くなります。

ベンチマークのコードはリスト5で,実行結果は以下です。

             Rate     args no_args
args      80645/s       -- -98%
no_args 3333333/s    4033% --

リスト5 Smart::Argsのベンチマーク

use Smart::Args;
use Benchmark qw(cmpthese);

sub no_args_func {
    my ($num1, $num2, $num3, $num4) = @_;
    return $num1 + $num2 + $num3 + $num4;
}

sub args_func {
    args_pos my $num1 => 'Num',
             my $num2 => 'Num',
             my $num3 => 'Num',
             my $num4 => 'Num';

    return $num1 + $num2 + $num3 + $num4;
}

cmpthese(100000, {
    no_args => sub {
        no_args_func(100, 200, 300, 400);
    },
    args => sub {
        args_func(100, 200, 300, 400);
    },
});

チェックをしていないno_args_funcは変数への代入と加算しかしていないので当然速く,秒間333万回も実行できます。しかしargs_funcのようにNum型だけを使ったチェックを入れるだけで,だいたい秒間8万回ほどしか実行できなくなります。

秒間数万回実行できるのは十分速いですが,非常に多く呼ばれる関数への型制約の導入は慎重に検討したほうがよいでしょう。

まとめ

本稿では,Perlで動的な型制約を導入する動機や,Smart::Argsを使って型制約を導入する方法,型制約の応用など,Perlの動的な型制約について紹介しました。Smart::Argsの導入は簡単ですし,少しの手間でアプリケーションの堅牢性を高めることができるので,ぜひ使ってみてください。

さて,次回の執筆者は岡林大さんで,テーマはPlack::Middlewareです。お楽しみに。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.110

2019年4月25日発売
B5判/168ページ
定価(本体1,480円+税)
ISBN978-4-297-10533-4

  • 特集1
    名前付け大全
    設計も,実装も,ここから始まる!
  • 特集2
    速習gRPC
    高速! 安全! 高信頼! マイクロサービス接続の大本命
  • 特集3
    最新TLS 1.3徹底解剖
    通信を覗いてHTTPSの裏側を知る
  • 一般記事
    オープンデータのためのWikidata入門
    SPARQLで知識データベースを自在に検索!

著者プロフィール

柴崎優季(しばざきゆうき)

1989年福井県生まれ。株式会社はてな所属。

Webアプリケーションエンジニアとして,はてなブログなどのWebサービスの開発に携わってきた。

Twitter:@shiba_yu36
Blog:http://blog.shibayu36.org/