読者です 読者をやめる 読者になる 読者になる

IBMごときに何をうろたえるか

PHP

ちょっと前なんですが、
セキュアな PHP アプリケーションを作成するための 7 つの習慣
について、「大手さんだから安心」? - がるの健忘録 から、「セキュアなPHPアプリケーションを作成するための7つの習慣」のサンプルがとんでもなく酷い - ockeghem(徳丸浩)の日記 とか "セキュアな PHP アプリケーションを作成するための 7 つの習慣" を全力でDISる - id:k-z-h がリンクされていました。

うーん、スキだらけのセキュリティ講座が蔓延しているのは、PHP世界ではいつものこと…って思ってれば間違いないということじゃないかと思うのですが、それじゃだめなのかな。

以前、「Rubyの話題を出すと、コードの書き方に難癖つける人がやってくる」なんてことを話題にしました。ユーザーの傾向ってあるよね、と。そこでPHPなんですが、PHPの話題が出ると決まって、セキュリティに関して終わらない議論が始まってしまう。しかも、決着つかないまま、しばらくするとまた同じような話題が現れるのを繰り返してる。そんな風に見えるんですね。

だいたい決まって、こんな感じ。

  • htmlspecialcharsは、エンコーディングを指定しない場合に完全なエスケープができない説。
  • SQLインジェクションから防衛するからって、magic_quote_gpcは最悪だとかなんとか。
  • 変数にエスケープ関数かぶせても、SQL文に文字列結合したら台無し。
  • サニタイズって単語が出ると、決まって、リクエストヘッダ全部検査する説が出る。
  • スケールと公開/非公開を定義せずにセッションを議論して泥沼に。
  • サンプルコードの「意図」を無視し、「ふるまい」「流儀」のスキを突いてバカにする。

うち、最後のがけっこう気になります。他分野のプログラミングの話題だと、読むほうが「あぁ、言いたいことを目立たせるために、他のところは妥協したんだな」と好意的に解釈する(または、「まあ、ライターは優秀じゃないけど、役に立つ知識だけ仕入れたらいいや」と流しておく)はずの箇所に、PHPだと妙に食いつく人が多いような気がするんです。

悪く考えれば、「ケッ、あいつの記事は所詮その程度か、批判してる俺のほうが優秀だ」ってことをアピールしてるように見えなくもないですね。でまた、その批判が中途半端だと他人に批判されたりして。

ま、それじゃ悪意がありすぎるので、好意的に解釈して、

説明も微妙なところが多いが、サンプルが酷い。こんなサンプルでは悪い習慣が身についてしまう。

が正論だとしましょう。ごもっとも。でも、この理屈が重要なんだとすると、それって、「PHPユーザにはコピペプログラマーが多い」ということを暗示しているように思うのです。べつに初心者ユーザがみんな悪いわけじゃありません。いろんなレベルの人がいて当然だから。むしろ、「なぜPHPは悪質なコードのコピペが増えるか」を考えるのが重要なんじゃないかと思います。

PHPはひとつのタスクを書くとき、他の動的言語と比較して、記述が複雑になりやすい言語です。端的な単語として綴れない識別子、例外的な仕様の多さ、APIデザインの対称性の欠如、暗記しないといけないパターン、貧弱な構文、などでコードの冗長性が高くなります。ここまでは、構文パーサの速さの裏返しでもあるので、それ自体は許すとしましょう。

ただ、ここから後が問題です。本質部分より形式的手続き部分のほうが多い場合、優れたテキストエディタでゼロから書くよりも、テンプレートをコピーして、それを書き換えるほうが生産的になってしまいます。現実にたとえれば、フォーマルな手紙を書くとき、文例集が役に立つのと同じですね。PHPには「前略」がない。というわけで、PHPは雛形コードのコピペが起こっても仕方ない開発環境だと思うのです。

大問題はその次。「どこを見渡しても、綺麗なコードスニペットがない」うえに、「なかでも、PHPマニュアルに書かれたサンプルコードが一番無神経」ってこと。

IBMの記事にあるわかりやすい問題部分、

<?php
$link = mysql_connect('hostname', 'user', 'password') or 
        die ('Could not connect' . mysql_error());
?>

(これだとエラーメッセージが丸見えになってしまう。普通、インターネット公開サイトなら、レスポンスには単に「サーバに障害が発生したかも」とだけすべき。エラー詳細は、可能ならログファイルに記録したほうが良いけど、できなければ何もしないほうがマシ)

…についてなんですが、これ、

<?php
$link = mysql_connect('localhost', 'mysql_user', 'mysql_password');
if (!$link) {
    die('接続できませんでした: ' . mysql_error());
}
?>

と同じですね。下のほうのコードの出所はどこかって? 公式のマニュアルですよ。マ・ニュ・ア・ル。
ええそうです、心配後無用。IBM様がやるまでもなく、PHPのマニュアルの時点で、ユーザはすでに「悪い習慣が身につくような酷いサンプル」に触れています。

ちょっとIBMの味方になって考えてみますね。あのサンプルコードがそのまま実在のシステムに含まれてたら、ひどく軽率ですが、

<?php
echo '<p>' . $select . '</p>';
?>

あたりのSQL文を毎回レスポンスに含むあたりを見ると、「いいか、これは絶対にコピペで使えるコードじゃないからな。言わなくてもそれぐらいわかるだろう、読者諸君」と読めるので、ちょっと確信犯めいた部分も見えます。
※ 天然でエラーレスポンスにSQL文を書いちゃう、どうしようもない人もいるけど(w

そこまで考えると、PHPでのコピペは「良いコードスニペットを使って仕事を早くやる」場合限定ですね。読むために書かれたコードまでもコピペの対象に含んではいけない。読むためのコードは、本当に言いたいことが書かれた部分と、動作実験のお膳立てをするためだけの部分からできていて、前者は意味がわかればコピーするまでもないし、後者はまさに捨てるためにある。この違いがわからないようなスキルで、プロダクションコードを書くべきではないと思います。

初心者はコード書くなって? いやいやご心配なく。他にもプログラム言語をたしなんでいて、ちょっと文章を読む力があれば、すぐに見分けはつきますよ、たぶん。PHPで公開サイトやるのは、その後でも全然遅くないんじゃないでしょうか。

文章読ま(め)ない → 経験がなくてカンがはたらかない → でも仕事でとりあえず動くコードが要る → だからコピペする

というありがちな光景は、むしろPHPだからこそ、より強く、許されない行為なんじゃないかと思います。その意味で、書く側より、受け手に問題があるというのは、なんとなく感じて、PHPユーザはそれをうっすら自覚してると思うのです。なんか、コンプレックス?

なんで、なんか、読む側の自己責任に任せて、下手なコードにあんまりガミガミいうのは、火に油な気がするんですよね。「いまどきそんなの信じてちゃダメ、遅れてる〜」→「オレ本当にダメかもしれない」→「お、もっとバカがごろごろいる、オレでも偉そうにできる」→ 最初にもどる …みたいな。って、このエントリもその火に油注いでるのかな。

あ、もしや…
逆に、問題のある「受け手」が投入されるプロジェクトが採用してるのがPH… ごにょごにょ


以下余談

IBMの記事は、PHPのマニュアルのコードをベースにしていて、それが確信犯的やり口なのか、重点を際立たせるための妥協なのか、それとも、本当にライターの経験値が低い(無意識にセキュリティホールを作ってしまう初心者と同程度)のかはわかりません。ここでそれを断定してしまうのは軽率なので、やめておきます。ただ、彼が、「前のコードと後のコードを比較してみると、意味が違う部分が3箇所あるだろう」と言いたかったことだけを汲み取れば、それでいいんじゃないか、と。

SQL文の文字列結合で、値のクォート囲みを徹底するか否か、というような議論

数値文字列をエスケープ処理して、クオートしたものをSQLとして用いる方法は大垣靖男氏が提唱されている方法で、私はこの方法に賛成ではない(徳丸浩の日記 - 変数に型のない言語におけるSQLインジェクション対策に対する考察(5) - 数値項目に対するSQLインジェクション対策のまとめ参照)のだが、きちんと動作はする。しかるに、クオート抜きでエスケープのみするのでは、何の意味もない。

は、「列名はユーザに決めさせるな」とか「入力を検査しろ」とか「エスケープせよ」とか、その程度のところから説明しないといけない初心者に対しては、だいぶ後から言うことじゃないかと思います。

そこで、データベースを保護するための習慣を身につけるには、動的な SQL コードを可能な限り避けるようにすることです。動的な SQL コードを避けられない場合には、…

というくだりを読めば、単に、プリペアドステートメントかクェリビルダかORMかを使うのがいいことは解っている、けど、どうしようもない場合があって、そのケースは、往々にして、知識のない人の近くで発生するから、一番最初に必要な基本だけ説明するよ、と言いたいだけじゃないかと。

<?php
    if (isset($_POST['account_number']) &&
    isValidAccountNumber($_POST['account_number'])) {
        // (ry
        $select = sprintf("SELECT account_number, name, address " .
		" FROM account_data WHERE account_number = %s;",
        mysql_real_escape_string($_POST['account_number']));
?>

あえて、このサンプルコードのまずいところを挙げるなら、余計なmysql_real_escape_stringを付け足してしまったことでしょう。isValidAccountNumberで数値として妥当であることを確認したんなら、そもそもエスケープは不要ですね。無駄なコードは読むためのコードの意図をにごらせます。

ま、入稿ギリギリで、どうしても文字列をエスケープすることを説明しないといけなかったんでしょうね。たぶん。こんな付け足しがありますよ。

(いったん話が完結したあと)
またこの例は mysql_real_escape_string() 関数の使い方も示しています。この関数は入力に無効な文字が含まれないように入力を適切に検証します。magic_quotes_gpc に頼っている方には、…

あ〜、やっぱり対象読者がマジッククォートのレベルなのか、と。

ファイルシステムについても、

ファイルシステムへのアクセスをユーザー入力と一緒に構成することは危険なため、そうした構成を完全に避けるようにした方が賢明です。そのためには、データベースと、生成される隠しファイル名を使うようにアプリケーションを設計します。しかし必ずしもそうはいかない場合もあります。

またまたこの論法。基本は「するな」と書いてあるのを見落としがち。まあ、この、「必ずしもそうはいかない場合もあります」にPHPの現実が見え隠れしてるような気も…。

ともあれ、「たとえばリクエストされるファイル名を一定のパターンにはめれば、パス区切り文字のような記号を注入されることはないよね」って言いたいだけなのに、なんだか、「それでは包括的な視点でみたときに安全性を確保できたとはいえない」なんて言われても、ねぇ。中学生相手にニュートン力学で運動の話してる最中に、熱力学や大気摩擦を持ち出すようなことはやめましょうよ。社会人になるまでには、熱も流体もやるからさぁ。

IBM記事全体を通してまずいところは、要点以外のコードの質が低いということを明記していないことと、7項目で全部だと言い切っているように読めること。なんか、日本人なら、「これはあくまで基本で、もっと多様な防御方法があることを忘れないで欲しい」とか書きそうなのに、アメリカ人って横柄だからなぁ〜。まあでも、初心者に注意喚起するだけなら…


… あれ?

レベル: 中級

うそ〜ん