「脆弱性を局所的に潰す」はアンチプラクティス

(Last Updated On: )

本物の「セキュアコーディング」(セキュアプログラミング)を知ればもう議論など必要ない、と思っています。本物の「セキュアコーディング」を「知ろうとしない」と幾らでもアンチプラクティスを作ってしまいます。

議論は終り、と思っていたのですがそうも行かないようなので紹介します。セキュアコーディングの概念を全く知ろうとしないで、セキュリティを作るのは「ただの無駄」です。多数のアンチプラクティス/間違いが含まれるスライドの中から(一々指摘するとキリがない)

  • 「脆弱性を局所的に潰す」

これ”だけ”だとアンチプラクティスです。

※ 契約プログラミングが流行らない理由はこいうアンチプラクティスも原因でしょう。特定の良いこと”だけ”を積み重ねても良い結果にならない、ことは合成の誤謬として知られています。

PHPカンファレンス2017 「著名PHPアプリの脆弱性に学ぶセキュアコーディングの原則」

今回の教材のスライドはこれです。

https://www.slideshare.net/ockeghem/php-conference-2017

一々指摘するとキリがないので、一気に最後の方に飛びます。

「安全なアプリケーション作り方」のまとめとして次のスライドあります。

「局所的に脆弱性を解消する」のが原則だとしています。その上で「局所的に安全性がとれるなら「値の安全性」は気にしない」が原則?!だそうです。

その代表例がプレースホルダによるSQLインジェクション対策だとしています。

まとめると

「出力先で誤作動が起きない場合は、データの妥当性など気にせず”局所的”に処理してしまう!つまり、他の箇所でのセキュリティ対策は要らない(他は追加の対策で本来必要ない)」

ということですね。

p.36で「なんかややこしいですね もっと簡単にしませんか?」とあるのように、ややこしいので「簡単」に考えたらこうなった、ようです。ややこしいのは信頼境界線の引き方が間違っていたり、セキュアコーディングの原則を使わなかったりしていることが原因ですが、いちいちアンチプラクティスを指摘すると膨大になるので今日のレビュー対象に集中します。

「局所的に脆弱性を解消する」はモグラ叩き

「出力先で誤作動が起きない場合は、データの妥当性など気にせず”局所的”に処理してしまう!つまり、他の箇所でのセキュリティ対策は要らない(他は追加の対策で本来必要ない)」

こうまとめると、最後にオマケ程度に「バリデーションは重要」と書いてある、と主張されるのかも知れませんが、データの妥当性をまず保証するのがセキュアコーディングです。

局所的に安全な方法がとれるなら、「値の安全性」は気にしない(原文のママ)

これをやってしまうとモグラ叩きになります。

単純にダメなコードになるのではなく、2017年版OWASP TOP 10だと脆弱なアプリケーション認定されます。Webセキュリティ専門家が脆弱アプリ認定される方法を原則とするとは信じがたいです。アプリケーションは不正な値(バリデーションエラー)を記録し適切な対応をすることが求められています。無視は脆弱なアプリとされます。

このスライドの前提条件としてセキュアコーディング第1原則「入力バリデーション」(=ホワイトリスト型の対策)はありません。データの妥当性など気にせず”局所的”に処理、するので攻撃者は”開発者が想定した部分以外”へ攻撃用のデータを送ることができます。

局所的=ブラックリスト型の対策は常に脆弱で容易に致命的問題のメンテナンスが必要になります。(すでに、これだけでもアンチプラクティスだと分かりますが、無視して続けます)

PostgreSQLやMySQLやなど「強いデータ型」を持つデータベースがバックエンドだったとします。整数、浮動小数点など決まったデータ型を持つ場合、別のデータ型を保存しようとすると致命的エラーでSQLクエリは実行されません。

失敗するモノはできる限り早く失敗させなければなりません。

  • アンチプラクティス: プログラミングの基礎原則、Fail Fastを無視する

この時点でもアンチプラクティスだと分かるのですが、アンチプラクティスにアンチプラクティスを重ねる構造になったスライドなので、ここでもまた無視します。

データを出力時に「局所的」に無害化!?

例えば、サーバーレスで動作させるため、「弱いデータ型」を持つSQLiteに変えたとします。(備考:SQLiteのカラムは基本文字列型)SQLiteは整数、浮動小数点型のカラムであって文字列が保存できます。

つまりプリペアードクエリを使っても致命的なエラーにならず、整数や日付カラムに「不正な攻撃用文字列を保存可能」です。プログラムが再度読み込んだ時、攻撃用データ/おかしなデータが入っているかも知れないので、ソフトウェアの全ての箇所で「整数」が必要なら「整数」であること、「日付」が必要なら「日付」であることを検証しないと正しく処理できません

SQLiteと同様に一般的なビューもデータ型など気にしてくれません。

ビューに出力する場合、(ビューはデフォルトHTMLエスケープである前提)

ID:<script>alert(1)</script>
日付:<script>alert(1)</script>

と出鱈目な表示をしてしまうかも知れません。(スライドの著者的にはJavaScriptがインジェクションできていないのでOKなのでしょう。一般的にはこれはバグとして、プログラマが対応することになるでしょう)

この著者の方式に従うと「入力データの妥当性は気にせず、問題になるケースを一つ一つ局所的に潰すのが良い」のでビューに出力する場合を疑似コードで書くと

$valid_id = validate_id($myid) // 無効なIDで例外、処理を停止
$valid_date = validate_date($mydate) // 無効な日付で例外、処理を停止
render_view()

とこんな感じになります。(Fail Fast原則無視のアンチプラクティスですが、ここでもスルーしてください)

ある時、システム負荷を軽減するためmemcachedを導入することになったとします。memcachedもデータ型など気にしないシステムなので、以下のようなコードになります。

$valid_id = validate_id($myid) // 無効なIDで例外、処理を停止
$valid_date = validate_date($mydate) // 無効な日付で例外、処理を停止
save_to_memcached()

ある時、メールでこれらの値を使う事になりました。コードはこうなります。

$valid_id = validate_id($myid) // 無効なIDで例外、処理を停止
$valid_date = validate_date($mydate) // 無効な日付で例外、処理を停止
send_email()

ある時、チャットに送ることにもなりました。

$valid_id = validate_id($myid) // 無効なIDで例外、処理を停止
$valid_date = validate_date($mydate) // 無効な日付で例外、処理を停止
send_chat()

ある時、、、もう面倒なのでここで止めます。そこら中でバリデーションだらけになります。こんなダメコードには、どこかでデータバリデーションが抜けていると、中途半端に処理が中止し、中途半端にデータが送信/保存される問題もあります。

この単純な例から分かるように、プログラムのそこら中で「モグラ叩き大会」が始まります。この例では文字列のケースを省略しましたが、電話番号、ユーザー名、グループ名、etc、etc。形式を持つ文字列も全てバリデーションが必要になります。

何故なら、インジェクション攻撃”さえ”できなければ、データの妥当性など気にせず”局所的”に処理する、が著者の”ベストプラクティス”だからです。どうやらインジェクション攻撃”さえ”できなければ、出鱈目な出力もOKなようので、著者的には”出鱈目な出力のまま”でも構わないのかも知れません。(このことは著者がSQLプレイスホルダの時は、と書いていことから分かる)

インジェクション攻撃しか気にしていないセキュリティ専門家には出鱈目な出力でも構わない、のかも知れませんが普通の開発者は困ります。遊びで作るプログラムなら構いませんが、数値、日付、電話番号、ユーザー名などに”プログラムコード”が書かれていても平気な開発者はあまりないでしょう。

そうなると「局所的」に沢山のデータバリデーションを入れることになります。構造も設計もないまま、このようなモグラ叩きコードを書くべきではありません。

変数を全て利用前にバリデーションするのは「契約プログラミング」の実行効率を無視した開発モードを運用モードで使うこととほぼ同じです。こんな「契約プログラミングもどきのコード」を書くくらいなら、本物の契約プログラミングの構造/設計を導入するのが得策です。

基本構造がない方法論で、上記のような「構造なしの局所的対策をモグラ叩きで作る」と、「繰り返し同じことを検証するダメなコード」が出来上がります。

ロジックの中で入力データバリデーション?!

これも、そもそもFail Fast原則を無視しているのでアンチプラクティスですが無視します。ここでは論理以外は重要でないので細かい内容も無視します。

先ほどの出力時に「構造なしの局所的対策をモグラ叩きで作る」と同じことがロジックの中”だけ”で入力データバリデーションを行うと発生します。

(入力データバリデーションがないので)大きすぎるパスワードが問題になった、パスワードチェックロジックで対処しよう。

(入力データバリデーションがないので)大きすぎるIDが問題になった、◯◯モデルで対処しよう。

(入力データバリデーションがないので)大きすぎる日付データが問題になった、◯◯モデルで対処しよう。

(入力データバリデーションがないので)大きすぎる検索文字列が問題になった、◯◯モデルで対処しよう。

(入力データバリデーションがないので)□□入力の攻撃用文字列が問題になった、◯◯モデルで対処しよう。

このように「構造なしの局所的対策をモグラ叩きで作る」こと、しかも問題になってから対策する、ことが多くなります。(ブラックリスト型対策なのでこうなる)

最後の方に「入力バリデーションはしましょうね」と書いてありすが、必須だとしていません。「入力バリデーションはセキュリティ対策ではない/必要ない」からは格段の進歩ですが、必須であるものを「しましょうね」ではNG、セキュアプログラミング/契約プログラミングの解説だと話にならない解説です。

ロジックが入力データバリデーション無しを前提とする場合(= プログラムが処理できないデータが入り放題)だと、ロジックでも出力と同く、そこら中にバリデーションコードが散らばります
(ロジックにはロジックのバリデーションが必要です。必要なら多層防御としてデータバリデーションも推奨です。参考:バリデーションには3種類のバリデーションがある 〜 セキュアなアプリケーションの構造 〜

「構造なし」の対策は脆弱かつ事後対応型のダメコードを作ります

契約プログラミングも紹介してあるが・・・

「表明が実行時に無効化されることが問題」としていますが、これは契約プログラミングを理解していない初心者に多い勘違いです。

「表明が実行時に無効化されることが問題」ではありません。契約プログラミングに於て、本番運用時に無効化されるassertionにするか、それとも本番運用時に必要なコード(契約チェックを行うまたは契約チェックが必要ないコード)にするか、は「セキュリティ構造の設計その物」です。

「セキュリティ構造の設計その物」なので間違いがあると脆弱性ができるのは当たり前の話で、「表明が実行時に無効化されることが問題」ではなく「誤りのあるセキュリティ構造/設計が問題」です。

正しくは「表明によらないセキュリティ構造を作らないと脆弱になる」などとしないと契約プログラミングを知らない人だと何をして良いのか分らなくなります。要するに以下のような構造をどのように、ソフトウェアの設計、としてつくり込むかがセキュリティ構造/設計です。

この図の様に、契約プログラミングで普通に論理的な構造と設計を作ると、自然にセキュアコーディング第1原則(入力をバリデーションする)と第7原則(出力を無害化する)が徹底されます。

契約プログラミングでも入力「バリデーションはしましょうね」ではありません。「入力バリデーションは必須(しなければならない)」です。入力バリデーションをしないと脆弱になります。セキュアコーディング第1原則(入力をバリデーションする)が「しましょうね」ではありません「必須」です。

ロジックバリデーションやそもそもセキュアなロジックが必要な所で、入力バリデーション(基本はデータ形式のチェックだけ)で役立たないケースがあるのは当たり前の話です。入力、ロジック、出力、それぞれに役割があります。表の◯×は不要です。表にするなら、入力、ロジック、出力を書かないと「入力バリデーションは重要でない」とする誤解の元でしょう。(それが狙い?)

契約プログラミングは開発時に、コード中に契約チェック(つまりバリデーション)を行います。運用時にはほとんどの契約チェックは無効化されます。”ほとんど”でないチェックがバリデーションとして残ります。

契約チェックの最上流となる「アプリケーションへ”入力”のバリデーション」は運用時に必須チェックとして残ります。信頼境界だからです。

契約チェックの最下流となる「アプリケーションからの”出力”のバリデーション(や他の無害化)」も必須チェック/処理として残ります。信頼境界だからです。

運用時に残る契約チェック(バリデーション)が「セキュリティ構造/設計」になります。入力と出力は基本構造として必須の構造でこれを欠く場合は脆弱な構造/設計です。Fail Fast原則に則り、失敗するものは出来る限り早く失敗させる、ことを忘れないようにしましょう。当然ですが、ロジックによる論理バリデーションも残すことが必須です。これがないとプログラムになりません。

「アプリケーションの前提条件としてバリデーションは重要」は当然です。コンピュータープログラムは、例えロジック/アルゴリズムが正しくても、妥当なデータでしか”原理的”に正しく動作できないです。

この著者は過去に「入力バリデーションは必要ない/セキュリティ対策でない」「文字エンコーディングバリデーションも必要ない(言語などがすべき、としているがそれは一般的でない)」としていたので「重要」とした点は大きな進歩ですが

  • 入力バリデーションは単に「重要」なのではなく「必須」です

セキュリティ専門家なら誰でも知っていることを、10年近くも必要ない/セキュリティでない、と主張されてきた責任を感じるなら「必須」と明記すべきでしょう

文字エンコーディングバリデーションの事は都合が悪いのかスライドに記載がありません。正しい文字エンコーディング処理には入力バリデーションが欠かせない、と理解っているからでしょうか?

まとめ

今では死語となった構造化プログラミング(gotoではなく関数/メソッドを使うプログラミング)が提案された時には、コンピュータサイエンティストの間でも議論の的となりました、「gotoも(は)必要だ」と。

私も時々、プログラムを書くときにgotoは使います。ただし、それはgotoを使った方が綺麗に書けるときだけ、例外です。gotoは必要です。しかし、「gotoは必要だから、一般プログラミングの原則としてgotoを使う」などという馬鹿なことはしません。他の開発者の皆さんも同じでしょう。

「gotoは必要だから、一般プログラミングの原則としてgotoを使う」は構造がないプログラムを作る、と同類です。

「(入力データバリデーションしない場合で)どうしても局所的にセキュリティ対策が必要になった箇所で対策を行うことが原則」は構造がないセキュリティを作ります。

90年代に「防御的プログラミング」が提唱され導入されましたが、構造がないセキュリティ対策が蔓延したため、セキュアコーディングが考案されました。
(因に、セキュアコーディングはコンピュータサイエンティストの間で議論になったと承知していません。セキュアコーディングは当たり前の「コンピュータの動作原理」=「妥当なデータ/ロジックでしかプログラム正しく実行できない」に基づいているので議論になるはずがありません。この原理に基づかないモノはセキュアコーディングではない、別の何かであり、セキュアコーディングだ!と主張しても紛い物です。ここにこんなブログを書いているのも馬鹿馬鹿しいですが、紛い物で開発者がサイバー犯罪者やセキュリティ業者の餌食になることは放置できません)

CERTセキュアコーディングは、プログラムが「正しく動作」すること保証する構造、を作ってソフトウェアセキュリティを維持します。

  • 入力バリデーション(原則1)
  • セキュアコーディング標準による安全なロジック処理(+具体的な入力/出力処理)
  • 出力の無害化(原則7)

3つが1つのセットとして成り立ちます。これ以外はセキュアコーディングではありません。

セキュアコーディング原則の第一原則(他の原則も)を無視すると、セキュアコーディングに準拠したコーディング/設計/構造とは全く言えません。

構造がないプログラムはメンテナンス性に欠ける上、バグの温床になることを開発者は知っていると思います。今のプログラムには「プログラム処理の構造化」に加えて「セキュリティ処理の構造化」も必要です。「セキュリティ処理の構造化」はセキュアコーディングの概念で作ると、自然かつ効率的に構造化できます。

総括するとこのスライドの内容は様々なアンチプラクティスを推奨し、必須のベストプラクティスをオプションであるかのような誤解を招く危険なスライドになっています。ベストプラクティスもどきのアンチプラクティスです。

包括的に対処できる(=入力バリデーション)のに、個別/局所的に対応するのはアンチプラクティスです。

バリデーションには3種類ありそれぞれに役割がある、これを知らなかったら簡単にアンチプラクティスに騙されたかも知れません。バリデーションの種類、重要です。

IPAのセキュアプログラミングでも「入力バリデーションが第1実装原則」p.11 として記載されました。もう日本国内でも言い訳はできません。

最後に、ソフトウェアの信頼境界線の引き方の間違いくらいは直しましょう。

ユーザーDBは「ソフトウェアの信頼境界の外」で中に入れることができません。「ソフトウェアの信頼境界」の最大限は独立して動作するアプリケーション単位までです。「外」のデータベースは「外」です。もし、SQLiteなんだ、と強弁しても、セキュアコーディング原則ではSQLiteのようなモノは信頼境界の外だと明記しています。

信頼境界の引き方が間違っているのはセキュアコーディング以前の問題です。

複数の独立したソフトウェア(アプリケーション)が信頼境界内にある場合、それは「システムの信頼境界」です。ソフトウェアセキュリティの話をする場合、適切な「ソフトウェアの信頼境界」を使うのは当然でしょう。

投稿者: yohgaki