(Last Updated On: 2019年2月18日)

このブログではどのように”データ型”の概念とセキュアコーディングが関連しているのか、Webサーバーアプリを主体に説明します。

セキュアコーディングの基本を理解している必要がありますが、難しくはないです。原則を知っているだけで十分です。セキュアコーディングの10原則は次の通りです。

1番目の原則「入力をバリデーションする」がデータ型と関連します。

プログラミング言語の「基本データ型」

一般にプログラミング言語は基本データ型として

  •  文字列
  •  整数
  •  浮動小数点
  •  配列/リスト

を持っています。この他に

  •  クラス/オブジェクト/構造体など複合型

があります。これらは基本的には文字列/整数/浮動小数点/配列/リストを組み合わせ”ユーザーが定義するデータ型”で、文字列/整数などの基本的なデータ型とは異なります。

プログラミング言語は一般に”汎用”利用されることを前提に設計されています。この為、基本データ型は汎用データ型として、そのデータ型が許すデータなら何でも保存できます。

例えば、C言語の整数型に対してオーバーフローするような値を代入しても「許されている」ので、無効なデータであっても保存できます

クラス/オブジェクトをサポートする言語には、言語要素として予め定義されたクラス/オブジェクトが在る場合もあります。こういった場合も基本的には”汎用”データ型として定義され、そのクラス/オブジェクトが許すデータなら何でも保存できます。

アプリケーションの基本データ型とは?

アプリケーションの入力データは、プログラミング言語の「基本データ型」が許可しているデータを全て受け入る変数ばかりではありません。プログラミング言語の「基本データ型」がそのまま使えるケースの方が少ないです。

プログラミング言語の「基本データ型」は”汎用データ型”であり、アプリケーションのデータ型はのほとんどが”専用のデータ型”です。プログラミング言語の”汎用データ型”とは一致しません。

アプリケーションにとってオーバーフローする整数値は通常”不正なデータ”です。年齢に”999”や取得するレコード数に”1億”、は無効なデータです。都道府県名に”あいうえお”といった文字列データも無効なデータです。

Webアプリケーションのデータは基本的に”全てテキストデータ”として取り扱われます。HTTP/HTML/XML/JSON/SMTP/IMAP/LDAP/SQLといったインターフェースはデータをテキストとしてやり取りしています。整数データであっても”アスキーテキストの数字文字”でデータをやり取りします。

HTTP/HTML/XML/JSON/SMTP/IMAP/LDAP/SQLといったテキストインターフェースを持つ処理系の場合、整数データの”はず”のデータであっても、”アスキーテキストの数字文字”だけが送られてくる、とは限りません。アスキー英数字だけの”はず”のデータであっても”アスキーテキストの英数字文字”だけが送られてくる、とは限りません。

攻撃者は”整数であるはずの年齢や数量データ”に”プログラム”を当たり前に送ってきます。”英数字だけであるはずの商品コード”に”プログラム”を送ってきます。攻撃用データは”プログラム”に限りません。アプリケーションやライブラリにとって”特別な意味を持つ文字”(改行/ヌル文字などの制御文字や壊れた文字エンコーディングなど)を含めたデータを送り攻撃してきます。こういった攻撃はインジェクション攻撃として知られているWebアプリケーションの最大の脅威になっています。

無効なデータはどこまで行っても無効なデータのままです。誤作動の原因でしかありません

※ 原則として、無効なデータのフィルタリング/サイニタイズは脆弱性に分類させる脆弱な仕様です。標準入力バリデーションにはフィルタリング/サニタイズは含まれません。

話が少しそれましたが、アプリケーションは”専用”ソフトウェアで、それぞれの入力に”専用のデータ型”を持っている、と言えます。

  •  ユーザーID(整数) – 1000以上、2^63未満の整数
  •  商品コード(ASCII文字列) – 10桁の英数字
  •  名前(Unicode文字列) – 制御コード/記号文字を除く、1文字以上、20文字以下のUTF-8文字データ
  •  ふりがな(Unicode文字列) – 1文字以上、40文字以下のUTF-8平仮名
  •  メールアドレス(Unicode文字列) – ”RFC2822で許可された一文字以上のユーザー名文字列@有効なドメイン名”の形式を持つ文字データ

このような形で”アプリケーションの入力データ”には”専用のデータ型”があると言えます。

「入力データをバリデーションする」とは

 言い換えると「”アプリケーション専用のデータ型”として、”アプリケーションビジネスロジック”として妥当であることを保証する」です。

特定目的に開発された専用ソフトウェアであるアプリケーションには”アプリケーション専用のデータ型”があります。通常、この”アプリケーション専用のデータ型”は”プログラミング言語の基本データ型”とは完全互換ではありません

一般に”プログラミング言語の基本データ型”は”アプリケーション専用のデータ型”とは完全互換ではなく上位互換であり、アプリケーションにとって”無効なデータ”でも処理してしまいます。そして、アプリケーションが入力データをバリデーションせず”アプリケーション専用のデータ型”であることを保証していないこと、がインジェクション攻撃に脆弱になる大きな原因になっています。

インジェクション攻撃は”誤作動”の一種で、その大きな原因が”無効なデータ”です。このため、知る限りではIPAの旧セキュアプログラミングガイドを除き、全ての真っ当なセキュリティガイドラインでは、無効なデータを排除する為にホワイトリスト型の入力データバリデーションを行うことを要求しています。バリデーションの役割は有効なデータであることを保証し、アプリケーションの正しい動作を保証すること、にあります。

重要なので繰り返します。無効なデータはどこまで行っても無効なデータのままです。”誤作動”の原因でしかありません

「入力データをバリデーションする」をデータ型を使って言い換えると「入力データが”アプリケーション専用のデータ型”として妥当であるあることを保証する」(第一段階のバリデーション)となります。

「入力データが”アプリケーション専用のデータ型”として妥当であるあることを保証する」だけではまだ不十分です。”データ形式”がアプリケーションにとって妥当であるかしか検証していません。

入力データがアプリケーションにとって妥当であるには、入力データがアプリケーションにとって”論理的”に妥当でなければなりません。

  • 購入する商品のオプションが選択可能である
  • グループに追加するユーザーが存在する

こういった情報に付与されるID情報は”アプリケーション専用のデータ型”として妥当な形式であるあるだけ、では不十分です。「入力データが”アプリケーションのビジネスロジック”に対して妥当であるあることを保証する」(第二段階のバリデーション)が必要です。

「入力データが”アプリケーションのビジネスロジック”に対して妥当であるあることを保証する」には認証(Authentication)と認可(Authorization)、真正性(Authenticity)も含まれます。これらはビジネスロジックとして論理的に検証&保証されていなければなりまねん。認証、認可、真正性は”データ形式”(アプリケーション専用のデータ型)の検証では保証できないことは説明不要でしょう。

どこで2種類の入力データの検証を行うか?

 入力データの検証には次の2種類があると理解りました。

  1. 「入力データが”アプリケーション専用のデータ型”として妥当であるあることを保証する」(第一段階の形式的バリデーション
  2. 「入力データが”アプリケーションのビジネスロジック”に対して妥当であるあることを保証する」(第二段階のロジックバリデーション

※ 基本は上記の2種類ですが、同じアプリケーションであっても複数の箇所/異なるレベルで実施することがほとんどです。

これらの入力データ検証をアプリケーションの何処で行うべきでしょうか?

 セキュアコーディング原則1の「入力をバリデーションする」では”全ての信頼できない入力データをバリデーションする”ことを求めています。

Webアプリケーションクライアント(ブラウザ/Webサービス利用アプリなど)からの入力データは基本的に全て信頼できません。従ってブラウザなどからの入力データは全てバリデーションする必要があります。

入力バリデーション実装の原則:

  •  フェイルファースト原則で実装する(フェイルファーストとは失敗するモノはできる限り早く失敗させるプログラミングの一般原則。ただし、CWE-20でも言及している通り”データの正規化”は先に行う)
  • ゼロトラスト原則で実装する(全てを信頼しない原則。検証されたモノである、と完全に保証できないモノは信頼しない。検証さえも信頼しない – ISO 27001:2012では検証コード/プロセスの検証も要求)

 セキュアコーディングのベストプラクティスでは”全てのコンポーネントが入力バリデーションを行うよう指揮すること”が求められます。言い替えると”多層防御(縦深防御)を実装する”(セキュアコーディング原則8)になります。

Webアプリケーションの入力データバリデーション

Webブラウザからの入力の場合、以下の様に検証します。

  1. WebブラウザのJavaScriptで入力データバリデーションを行う
  2. Webアプリ(サーバー)のプログラムで入力データバリデーションを行う
  3. Webアプリのデータストレージ(RDBMSなど)で入力データバリデーションを行う

例えば、”年齢”というアプリケーションのデータ型(0以上140以下)がある場合、以下のようにバリデーションします。

  1. WebブラウザのJavaScriptで”0以上、140以下”であることを保証する
  2. Webアプリ(サーバー)のプログラムで”0以上、140以下”のデータのみ受け入れる
  3. Webアプリのデータストレージ(RDBMSなど)で”0以上、140以下”のデータのみ受け入れる

この例は、ブラウザ/Webサーバー/データベースの異なる独立したソフトウェア間での多層防御ですが、同じソフトウェア内でもモジュール間で同様の多層防御を実装するとより安全になります。 ※ ソフトウェアの信頼境界を越えるデータのやり取りが発生する場合、そのデータは全て信頼できません (=Webブラウザからの入力は全て信頼できない)

Webアプリ(サーバー)の入力データバリデーションは以下のように行います。

  • ユーザー(上記の場合、Webブラウザ)が送ってこれるデータだけを受け入れる
  • ユーザーが送ってこれるデータに十分な制限を課せない場合、Webアプリ(サーバー)で妥当な制限を課し、その範囲内のデータだけ受け入れる

 1のユーザーによる入力エラーのチェックをWebブラウザで行っている場合、2のWebアプリ(サーバー)では単純にアプリケーション仕様で決まっている”アプリケーション専用のデータ型”として妥当かバリデーションを行います。フェイルファースト原則に従わないと無用なリスクを生むことに注意してください。

1の「WebブラウザのJavaScriptで”0以上、140以下”であることを保証する」が実装できない/していない場合はどうするのか?と思うかも知れません。

1がない状態はビジネスロジックバリデーション(第二段階のバリデーション)をクライアント側で実装していない状態です。2のWebアプリ(サーバー)で両方実装(第一と第二段階のバリデーション)する必要があります。

MVCアーキテクチャーの場合:

  • フェイルファースト原則に従い、”コントローラー”で
    •  入力データのデコード/正規化を行う(デコード/正規化エラーは無効 = 処理中止。バリデーションの前にデコード/正規化を行うのはCWE-20の要求事項)
    • 入力データが”アプリケーション仕様”として許可する適当な長さの文字列であることをバリデーションする(Webブラウザが送るはずがないUnicode制御コードや未割り当て領域コード以外の妥当なUTF-8文字列で100文字以下、など。バリデーションエラーは無効 = 処理中止)
    •  上記の処理はルーティングなどの処理の前、可能な限り早く実施する
  • ビジネスロジックとして、モデルで
    •  入力データが”アプリケーション専用のデータ型”として妥当であるかバリデーションを行う(入力ミスは”妥当な入力”、つまり通常のエラー処理を実施する = 処理の強制中止は行わない)

ソフトウェアの各コンポーネント(この場合、ブラウザ/Webサーバー)がそれぞれの責任を果さないとWebサーバー側のデータバリデーション処理が複雑化します。各コンポーネントが厳格に入力データバリデーションを行うと、ソフトウェアをより単純にすることが可能(セキュアコーディング原則4)です。

データ形式バリデーションとロジックバリデーション

CERT、MITRE(CWE/SANS)、OWASPのセキュリティガイドラインでは入力データのバリデーションをデータ形式バリデーションとロジックバリデーションの2種類に分けて解説しています。

”年齢”のように単純な”アプリケーション専用のデータ型”の場合、他のデータとの整合性をバリデーションする必要はありません。しかし、”PCを注文する場合に選択可能なオプション”などの場合、データベースを利用し選択したオプションがビジネスロジック(ビジネスルール)に適合したオプションであるか検証する必要があります。(ソフトウェアの信頼境界を越えるデータには、基本的に再検証が必須である点に注意)システム構成によって”どこで、どのようなバリデーションを行わないとならないか?”は変わります。

”認証”と”認可”、”真正性”もバリデーションの一種で、ロジックバリデーションの1つです。ロジックバリデーションの多くはビジネスロジックを記述する部分、MVCの場合はモデル、に記述します。しかし、”認証”と”認可”、”真正性”のバリデーションはコントローラーで実施すべきである場合が多いです。(フェイルファースト原則)

  • データ形式バリデーション – MVCなら基本はコントローラー
  • ロジックバリデーション – MVCなら基本はモデル

ですが、これらは”基本形”であり、必ずデータ形式バリデーションはコントローラー、必ずロジックバリデーションはモデルで行う、といった原則ではありません。(原則にも例外がある場合もあります。原則を盲目的に信用するのではなくゼロトラストで考えます)

強いデータ型を持つプログラミング言語がよりセキュアでメンテナンスし易い理由

強いデータ型を持つプログラミング言語でプログラミングしている開発者であれば、データ型が異なると正しく動作しない、異なるデータ型で互換性がある場合でもキャスト(データ型を変更)しないと動作しない、と知っていると思います。上位互換のデータ型でなければ暗黙キャストでコンパイラが警告またはエラー(セキュアプログラミング原則2)にしてくれます。

強いデータ型を持つプログラミング言語では「言語がデータ型に適合しているか保証」してくれます。

データ型に不整合がある場合、絶対にプログラムは正しく動作しません。誤作動します。強いデータ型を持つ言語では「基本データ型」であれば、ほぼ確実に「基本データ型」として整合性があることを保証してくれます。「基本データ型」に整合性があることを保証できるだけでも、プログラムの誤作動をかなり減らせます。これが強いデータ型を持つプログラミング言語が

  • よりセキュアである
  • よりメンテナンスし易い
  • 大規模プロジェクトに向いている

と言われる理由です。

強いデータ型を持つプログラミング言語でも不十分

 強いデータ型を持つプログラミング言語でも「基本データ型」の整合性保証だけでは不十分です。”年齢”を符号付き整数型の変数に保存するだけで”年齢”データとして完全に妥当である、と考える開発者はいないはずです。

データ型によるセキュリティを実現するには、型理論(Type Theory)によるデータ型検証が必要です。 例えば、Scalaはコバリアント(Covariant)とコントラバリアント(Contravariant)を言語としてサポートしています。型理論のLSP(Liskov Substitution Principle)を実装する場合、これらの機能が必要になります。バリアンスサポート機能がない言語でもassertionなどを使ってある程度保証できますが、多少手間が必要で、実行時チェックなので型論理として正しいこと完全に保証するのは困難です。

アプリケーション専用のデータ型をクラスとして実装した場合に、そのデータ型が持つ”形式的検証”をセッターメソッドに実装すると実行効率が悪くなり現実的でなくなることもあります。例えば、100万を超えないカウンタ型のデータ型を定義した場合にセッターメソッドで毎回、カウンタ値が100万を超ていないかバリデーションした、とするとカウンタ値を増やすだけでif文によるブランチが発生します。「if文ブランチの1つくらいは大したことでない」と思うかも知れませんが、塵も積もれば山となる、で万事が万事この調子で実行時にバリデーションしているとアプリケーションの実行速度が許容不可能なレベルにまで低下することがあります。

話がそれ気味でしたが、要するに強いデータ型を持つ言語の「データ型チェック機能」は非常に有用だが、それだけでは不十分であったり、現実のアプリケーションでは利用できなかったりする場合がある、という事です。

  •  強いデータ型を持つプログラミング言語だからといって、自動的に型安全が保証される訳ではない
  • そもそもアプリケーションプログラマが適切な”アプリケーション専用のデータ型”を定義していないと型安全も保証できないし、データ内容の安全性も保証できない
  • Scalaのようにバリアンスをサポートする言語でも適切に型制約を定義&利用しないと型安全を保証できない

関連: 契約プログラミング/契約による設計はコードが持つ契約(≒”アプリケーション”が定義するデータ型)が妥当であるか、確実にバリデーションするプログラミング/設計手法でもあります。

投稿者: yohgaki