12月01日(火)

YouTubeにAPIでアップロードするとBroken pipeで接続を切られる問題

カテゴリ: Ruby, トラブルシューティング このエントリーを含むはてなブックマーク はてなブックマーク - YouTubeにAPIでアップロードするとBroken pipeで接続を切られる問題

YouTube Data APIの直接アップロードAPIを使用して動画のアップロード機能を実装していた時の話。ちなみに言語はRubyで認証にはRuby OAuthを使用している。それまではちゃんとアップロードで来ていたのに、いつの日からかYouTubeにリクエストを送ったところで以下のエラーが発生して失敗するようになってしまった。

Errno::EPIPE: Broken pipe
/usr/local/lib/ruby/1.8/net/protocol.rb:175:in `write'
/usr/local/lib/ruby/1.8/net/protocol.rb:175:in `write0'
/usr/local/lib/ruby/1.8/net/protocol.rb:151:in `write'
/usr/local/lib/ruby/1.8/net/protocol.rb:166:in `writing'
/usr/local/lib/ruby/1.8/net/protocol.rb:150:in `write'
...

なんだか分からないがYouTube側から接続が切られているらしい。色々変なものを上げ続けたためにBANされたのかと思ったが、別のホストから行ってもBroken pipeになるので、どうやら送りつけるリクエストの問題らしい。原因のヒントが無いに等しい状態なので、色々試行錯誤を行ったところ、なんとか原因を探り当てることができた。Broken pipeになる原因はこれだ。

「リクエストの形式が正しくない場合に、リクエストのサイズが2MBを超えるとYouTube側から接続を切られる」

2MB(正確には2.15MBくらい)よりも小さな動画ファイルを使用してアップロードを行うと、ステータスコードが「400 Bad Request」、ボディが「Incomplete multipart body.」や「No file found in upload request.」となっているレスポンスが返ってくるようになった。おそらく「不正なリクエストに長々と付き合ってられん」ということで、ある程度の長さになると問答無用で切断するようになっているのだろう。こういうヒントも何も出ない挙動はドキュメントに記載してほしい。

さて、肝心のYouTubeにリクエストの形式が正しくないとされてしまう原因だが、エラーメッセージからするとマルチパートのバウンダリがちゃんと認識されていないようだ。アップロードAPIへのリクエストはメタ情報のXMLと動画ファイルの2つのパートから構成されており、これを当初は以下のように生成していた。

body = ""
body.concat("--#{boundary_string}\r\n")
body.concat("Content-Type: application/atom+xml; charset=UTF-8\r\n")
body.concat("\r\n")
body.concat <<EOS
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">
  <media:group>
    <media:title type="plain">#{title}</media:title>
    <media:description type="plain">#{description}</media:description>
    <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">People</media:category>
    <media:keywords>Foo</media:keywords>
  </media:group>
</entry>
EOS
body.concat("--#{boundary_string}\r\n")
body.concat("Content-Type: #{content_type}\r\n")
...

ソースコードの改行コードはLFなので、2個目のバウンダリは「[LF]--boundary_string[CR][LF]」のようになる。これは「[CR][LF]--boundary_string[CR][LF]」のようになるのが正しいはずだ。ということでバウンダリの指定をbody.concat("\r\n--#{boundary_string}\r\n")にしたところアップロードできるようになった。おそらくうまく行っていた頃のYouTubeはLFだけの場合も許容してくれていたのだろう。それがいつかのタイミングで許容されなくなったと。こういう変更の情報がどこかに掲載されているとありがたいのだが、どこかにあったりするのだろうか。

11月30日(月)

MySQLのクライアント側からの正しい文字コードの指定方法

カテゴリ: MySQL このエントリーを含むはてなブックマーク はてなブックマーク - MySQLのクライアント側からの正しい文字コードの指定方法

先日まとめたようにMySQLの接続文字セットを設定する方法は以下の5通りの方法がある。

  • グローバルなオプションファイルで指定する
  • アプリケーション独自のオプションファイルで指定する
  • mysql_options()で文字セットを直接指定する
  • mysql_set_character_set()で指定する
  • SET NAMESで指定する

だが、「MySQL/Rubyにおける正しいエンコーディング変更方法 - しばそんノート」によると、mysql_options()のMYSQL_SET_CHARSET_NAMEではクライアント側の状態しか変更しないらしい。mysql_set_character_set()はmysql->charsetを設定しつつSET NAMESしているだけ、mysql_real_connect()には接続文字セットをサーバに送る機能は無いということは、接続文字セットの情報をサーバに送る方法はSET NAMESしかないと言っていいだろう。ということは同じく接続のタイミングで接続文字セットを指定するオプションファイルを使用した方法でも駄目なのではないだろうか。MySQLのソースコードを見て確認してみた。なお確認したソースコードは元記事よりも少し新しいバージョン5.1.41のものである。

オプションファイルの読み込みはCLI_MYSQL_REAL_CONNECT()関数の冒頭で呼び出されているmysql_read_default_options()関数で行われているようだ。オプションファイルは読み込まれた後キーは整数値にデコードされ、case文で処理される。その中の文字コード関連の処理は次の部分だ(数値が定数化されていない理由は分からない)。

sql-common/client.c mysql_read_default_options() 行1164〜

	case 18:
	  my_free(options->charset_name,MYF(MY_ALLOW_ZERO_PTR));
          options->charset_name = my_strdup(opt_arg, MYF(MY_WME));
	  break;

文字セット名がoptions->charset_nameに設定されるだけである。MYSQL_SET_CHARSET_NAMEによる指定はoptions->charset_nameからmysql_init_character_set()を経由してmysql->charsetに反映されるという経路で反映されるので、オプションファイルによる指定も同じ経路だと言えるだろう。したがって、オプションファイルによる指定もまた、サーバ側の状態を変更しないという結論になる。

ちなみに元記事にあるmysql_real_connect()で唯一見つかったmysql->charset->numberの参照部分の前後は以下のようになっている。

sql-common/client.c CLI_MYSQL_REAL_CONNECT() 行2277〜

  if (client_flag & CLIENT_PROTOCOL_41)
  {
    /* 4.1 server and 4.1 client has a 32 byte option flag */
    int4store(buff,client_flag);
    int4store(buff+4, net->max_packet_size);
    buff[8]= (char) mysql->charset->number;
    bzero(buff+9, 32-9);
    end= buff+32;
  }
  else

プロトコルバージョン4.1では32バイトのオプションフラグの中に文字セット情報も含まれているようだ。今はサーバ側に何も伝わらないということはプロトコルバージョン4.1は古いプロトコルなのだろう。MySQLにあるskip-character-set-client-handshakeというオプションは、この機能を打ち消す為ためだと思われる。たとえサーバ側で設定していたとしても、クライアント側で接続文字セットが設定されていない場合、既定値であるLatin1がサーバに送られて、その値で上書きされてしまう。このオプションはその不幸な挙動を抑制する為のものなのだろう。skip-character-set-client-handshakeを指定することで文字化けが解消したという話は、強制的に接続文字セットがサーバ側の文字セットと同じ値に設定されるという副作用によるものだと思われる。サーバ側の設定を変更する為の遠回しな方法というわけだ。

結論としては、mysql_set_character_set()以外の方法を使う場合は、必ずSET NAMESを使用してサーバ側の状態も更新する必要があるということになる。もともとSET NAMESだけ使用して設定している場合は、それに加えてオプションファイルかmysql_option()関数で接続文字セットを指定する様にする必要がある。ライブラリ等の都合によって追加の指定などが出来ないことも多いから、あらかじめサーバ側の接続文字セットの設定を合わせておくというのが安全確実な手段と言えるだろう。

11月23日(月)

MacOSに対応していないairpenMINIをMacOSで使えるようにする

カテゴリ: その他 このエントリーを含むはてなブックマーク はてなブックマーク - MacOSに対応していないairpenMINIをMacOSで使えるようにする

airpenMINIは私が買った当初はMacOSに対応していなかったのだが、いつの間にかに対応バージョンが出ていた。対応したといってもMacOS X用のソフトウェアはかなりショボくて、受信機をUSBポートにさすと描いた物を問答無用で特定フォルダにTIFF画像として保存するだけのものだったりする。airpenMINIのサポートページからMacOS X用のソフトウェアもダウンロードできるようになっているが、ファームウェアが異なるらしく、それをインストールしても何も起らない(受信機の青い色は伊達では無いらしい)。実のところairpenMINIのハードウェアはPegasus TechnologiesのOEMで、そちらのサイトでもいくつかのソフトウェアが公開されている。その中のNoteTaker for Mac (Mac OS X 10.5)にファームウェアも含まれているので、それにアップデートすれば使えるようになるかもしれない。ということでチャレンジしてみた。

Frimware Update Wizerdでアップデートを実行しようとすると「既にバージョン11.73が入っているが1.76に入れ替えるか?」と訊かれてしまう。元が1.73なら順当なバージョンアップっぽいのだが、11.73とは一体。airpenMINIカスタムなファームウェアという可能性もありえるが、底までする必要性が感じられないと言うか、なんとなく表示のバグっぽいので気にせずアップデートしてしまおう。

ということで使えるようになったがびみょ〜な感じ。

10月05日(月)

MySQLの接続文字セット設定方法まとめ

カテゴリ: MySQL このエントリーを含むはてなブックマーク はてなブックマーク - MySQLの接続文字セット設定方法まとめ

MySQLを正しく使用する為には、クライアント側で接続文字セットを指定することが必要だ。クライアントライブラリに含まれるmysql_real_escape_string()などの関数がコネクションの文字セットの設定を参照して処理を行うからだ。接続文字セットの既定の設定はLatin1(ISO-8859-1)であるため、正しく指定されていない場合、多バイト文字の2バイト目以降に含まれるメタ文字と同じバイト値がエスケープされてしまい、不要な文字が挿入されたりSQLの文法エラーなどの不具合が発生する。

必須な割にどうやって設定するのかがまとまっていないので、ひとまずまとめてみる。

グローバルなオプションファイルで指定する

MySQLの設定は基本的にコマンドラインオプション経由で行うが、それらと同じ内容をmy.iniなどのオプションファイルから指定することもできる。グローバルなオプションファイルは /etc/my.ini などの固定の場所に置かれるため、オプションファイルの編集には管理者権限が必要になる。

クライアントの設定は[client]セクションの下に記述する。たとえばクライアント側の接続文字セットをシフトJISにしたい場合には以下のように記述する。

[client] 
default-character-set=sjis
アプリケーション独自のオプションファイルで指定する

接続時のオプションを指定するためのmysql_options()関数で指定できるオプションの中にMYSQL_OPT_LOCAL_INFILEというものがある。これを使用することでアプリケーション独自のオプションファイルを使用することができる。

MYSQL_OPT_LOCAL_INFILE
ユニットに対するオプションポインター
ポインターが附与されない場合もしくはunsigned int != 0を指さす場合、コマンドLOAD LOCAL INFILEは有効です。

様々なデータベースを一律のインターフェースで扱えるようにした抽象化ライブラリではmysql_options()のようなMySQLに依存するインターフェースは存在しない。そのようなライブラリでは接続に関する設定は接続時のオプションとして指定するようになっており、ライブラリによってはMYSQL_OPT_LOCAL_INFILEと同等の指定ができるようになっている場合がある。

たとえばPerlのDBIではDSNの一部として指定することができる(DBD::Mysql::connect)。

my $conn = DBI::connect
$dbh = DBI->connect('"DBI:mysql:database=testdb;host=localhost;mysql_local_infile=/var/www/my.ini", $user, $password);

PHPのPDOでは接続時のオプションとして指定できる(PDO_MYSQL)

$pdo = new PDO($dns = 'mysql:host=localhost;dbname=testdb, $user, $password, array(
    PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/var/www/my.ini',
));
mysql_options()で文字セットを直接指定する

mysql_options()関数で指定できるオプションの中には接続文字セットを直接指定できるMYSQL_SET_CHARSET_NAMEというものがある。このオプションを使用すればわざわざmy.iniを用意する必要は無い。

MYSQL_SET_CHARSET_NAME
char*
デフォルト文字セットとして使用すべき文字セットの名称。

だが、このオプションに対応しているデータベース抽象化ライブラリは無いようだ。

mysql_set_character_set()で指定する

接続後にmysql_set_character_set()関数を呼び出すことで接続文字セットを変更することができる。

mysql_set_character_set(&conn, "sjis")

これまで出てきた方法は接続時にしか接続文字セットを設定できなかったが、この関数ならば接続中に変更することができる。また、この関数を使用した設定方法が最も適切だとされる。ただし、MySQL 5.0.7以降という比較的新しいバージョンでしか使用できないのと、データベース抽象化ライブラリを使用している場合は呼び出す方法がないので、いつでも使えるという方法とは言えないところが厳しい。

SET NAMESで指定する

SQLのSET文で接続文字セットを指定することができる。

SET NAMES sjis

ただし、SET NAMESはサーバ側の状態しか変更しない為、mysql_real_escape_string()などの関数の挙動は一切影響を受けない。したがってSET NAMESを使ってもエスケープの問題は解決しない。

08月12日(水)

ActiveRecordのアソシーエションでfind()ではjoinsが使えるがcount()では使えない件

カテゴリ: Ruby, Ruby on Rails, トラブルシューティング このエントリーを含むはてなブックマーク はてなブックマーク - ActiveRecordのアソシーエションでfind()ではjoinsが使えるがcount()では使えない件

ActiveRecord 2.0.2での話。ActiveRecordではbelongs_toやhas_manyといったアソシーエション経由でも、経由しない場合と同じようにfind()メソッドが使える。アソシエーションを経由している状態ではすでに一つ以上のテーブルがJOINされている状態だが、:joinsオプションを指定しても、ちゃんとよしなに合成してくれる。ドキュメント的にはcount()メソッドでも同様に、のはずなのだが、:joinsオプションを指定しても無視されてエラーになってしまうのだった。無効なオプションだったら指定した時点でエラーになるはずなので、有効なオプションのはずなのだが......

find()とcount()で何が違うのか調べてみよう。find()の場合、JOIN句の生成はActiveRecord::Associations::HasManyThroughAssociation#construct_joins()メソッドで行われており、:joinsオプションが指定されていれば追加されるようになっている(引数custom_joins)。ちなみに % はStringクラスのメソッドで、sprintfと同じ機能を持つ。分かりづらい。

        def construct_joins(custom_joins = nil)
          ...省略...
          "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
            @reflection.through_reflection.table_name,
            @reflection.table_name, reflection_primary_key,
            @reflection.through_reflection.table_name, source_primary_key,
            polymorphic_join
          ]
        end

count()メソッドの方はまた別の仕組みがあり、アソシエーションによる絞り込み条件はスコープの概念を使用して行うようになっている。そして:joinsオプションは最終的にActiveRecord::Calculationsを経由してActiveRecord::Base#add_joins!()メソッドで処理されるようになっている。

        def add_joins!(sql, options, scope = :auto)
          scope = scope(:find) if :auto == scope
          join = (scope && scope[:joins]) || options[:joins]
          case join
          when Symbol, Hash, Array
            join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil)
            sql << " #{join_dependency.join_associations.collect{|join| join.association_join }.join} "
          else
            sql << " #{join} "
          end
        end

add_joins()メソッドでは、スコープで:joinsが指定されている場合はそれが優先されて、オプションで指定した方は破棄されるようになっている。アソシエーションの場合、スコープで確実に:joinsが指定されているので、オプションで指定した方は常に破棄されてしまう。つまりこれはActiveRecordのバグといって良いだろう。それにしてもDRY、DRYといっている割に同じような処理が複数あるのはいかがなものか。

ちなみにこの問題は最新版では既に解決している模様。とはいえ過去のバージョンでは割とどうしようもないのが厳しい。

http://github.com/rails/rails/commit/db22c89543f45d7f27847003af949afa21cb6fa1

05月02日(土)

PHPは数値をオブジェクトにキャストできる

カテゴリ: PHP このエントリーを含むはてなブックマーク はてなブックマーク - PHPは数値をオブジェクトにキャストできる

PHPには時々何の役に立つのかさっぱり分からない不思議な機能があるが、このキャスト機能もそのひとつ。

PHPはC言語風(どちらかというとJava?)のキャスト演算子を採用しており、その中にオブジェクト型へのキャスト演算子が存在している。つまり文法上は数値をオブジェクトにキャストする事も出来る訳だが、なんと実際にキャストすることができる。何が起るのかさっぱり予想できないが、とりあえず数値をオブジェクトにキャストすると以下のようになる。

<?php
$o = (object)123;
var_dump($o);
?>
object(stdClass)#1 (1) {
  ["scalar"]=>
  int(123)
}

PHPのマニュアルの「PHP: オブジェクト: オブジェクトへの変換」によると、stdClassの新しいインスタンスが生成され、そのscalarプロパティに元の値が設定されるとある。何の役に立つのかさっぱり分からないところが恐ろしい。

おそらく本命はNULLとArrayからのキャストで、その他の型の場合は単にエラーにしたくなかったために設けた仕様な感じがするが、そもそもNULLとArrayからのキャストも要らないような気もする。

01月27日(火)

ActiveRecordのMultiparameter Assignment機能はちょっと危険

カテゴリ: Ruby, Ruby on Rails このエントリーを含むはてなブックマーク はてなブックマーク - ActiveRecordのMultiparameter Assignment機能はちょっと危険

ActiveRecordには複数の構成要素からなるデータの入力支援機能が付いていて、たとえば日付の年月日時分秒をそれぞれ別の入力欄で扱えるようにもなっている。正式になんと呼ぶのか分からないが、仮にMultiparameter Assignment機構と呼ぶとして、その仕組みを調べていたら、なんだか微妙な設計になっていた。Multiparameter Assignment機構についての詳しい説明は「Multiparameter Assignment を理解する - Rails で行こう!」に書かれていたので、そちらを参照していただきたい。

Multiparameter Assignment機構を使うと、パラメータ群をActiveRecordが適切に処理し、属性のデータ型に対応するオブジェクトを生成して属性に設定してくれる。この機能を使用した場合に未入力と形式不正の区別が付かなくなるのは、形式不整の場合はオブジェクトが生成できずにnilになってしまうからだ(したがってbefore_type_cast系メソッドでも元の文字列の値は取れない)。

さて、パラメータ群からオブジェクトを生成する為には、どのパラメータが生成するオブジェクトのどの属性にあたるのかというマッピング情報が必要になるはずだ。Multiparameter Assignment機構を利用する際にそれに関する情報を何も指定する必要が無いことから、ActiveRecordが何らかの情報を持っているのだろうと考えるのが普通だ。だがActiveRecordは何も持っていないのだ。情報は全てクエリーに乗っているのである。

たとえばdataというオブジェクトのwritten_onというdate型の属性を年月日の3つの入力欄で入力させると、以下のようなクエリーがサーバに送られる。

data[written_on(3i)]=12&data[written_on(1i)]=2007&data[written_on(2i)]=6

data[...]はいつもの形だが、その中の丸括弧付きの名前を持つパラメータがmultiparameter nameとして特別扱いされ、Multiparameter Assignment機構に回される。括弧の内の数字はパラメータ列の位置番号、その後のアルファベット1文字がパラメータのデータ型として解釈され、以下のRubyの値に加工される。

{"written_on" => [2007, 6, 12]}

この処理の際に、各パラメータを文字列から適切なデータ型に変換する必要があるが、なんと、Stringクラスの"to_"+アルファベット1文字という名前のインスタンスメソッドを呼ぶことで変換しているのである。たとえば"[written_on(3i)]=12"というパラメータであれば、"12".to_iが呼ばれるという仕組みである。

そしてこのパラメータ列を使ってwritten_on属性にマッピングされているDateクラスがのインスタンスが生成される。生成方法はかなり大胆で、そのままDateクラスのコンストラクタに渡しているだけである。そして生成したオブジェクトを属性値としてセットして処理完了となる。

      def execute_callstack_for_multiparameter_attributes(callstack)
        errors = []
        callstack.each do |name, values|
          klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
          if values.empty?
            send(name + "=", nil)
          else
            begin
              send(name + "=", Time == klass ? (@@default_timezone == :utc ? klass.utc(*values) : klass.local(*values)) : klass.new(*values))

              ...

この処理で注意しなければならないのは、ActiveRecordはパラメータ列の個数や内容、型指定のアルファベット1文字を一切チェックしないことだ。したがって100個の引数を渡すようなリクエストが送られてくればコンストラクタに100個の引数を渡そうとしてしまうし、任意の"to_"+アルファベット1文字という名前のメソッドを呼び出そうとしてしまう。したがってパラメータ列のチェックはコンストラクタ側で行う、Stringクラスに"to_"から始まる4文字のメソッドを安易に追加しないといった配慮をする必要がある。

ちょっとした罠なのは、composed_ofで追加した属性についてもこの仕組みの対象になるところだ。composed_ofに指定したクラスのコンストラクタには任意の値が来ることを考慮しているはずだが、さすがに任意の個数が来ることまでは考慮していないかもしれない。コンストラクタにオプショナルな引数(しかも挙動を切り替えるような)があったりすると危険である。

01月22日(木)

ActiveRecordで日付型のカラムを参照すると時々DateTimeになる

カテゴリ: Ruby, Ruby on Rails, トラブルシューティング このエントリーを含むはてなブックマーク はてなブックマーク - ActiveRecordで日付型のカラムを参照すると時々DateTimeになる

Railsのアプリで突然comparison of Time with DateTime failedというエラーが出た。TimeとDateTimeは比較できないということのようで、エラーはデータベースから取ってきた日付値を現在時刻と比較しているところで発生している。現在時刻はTime.nowで生成しているので、こっちはTimeクラスのインスタンスで間違いないだろう。怪しいのはもう片方の方だが、ActiveRecordでは日付はTimeクラスになるはずで、ならなければこの部分はエラーで全く動かなかったはずだ。ではなんでいきなりDateTimeな値が?

RubyのTimeとDateTimeを比較した時に思いつく大きな違いは、Timeクラスで表現できる値はtime_t型の範囲に限られるというところだ。MySQLの日付型はtime_t型の範囲以上の値を表現できる。もしかするとActiveRecordは、Timeクラスで扱えない日付の場合に、頑張ってDateTimeにしてくれたりするのだろうか。

ActiveRecordでは属性にアクセスされた際に文字列から適切な型に変換するようになっている。それを行っているのがActiveRecord::ConnectionAdapters::Columnクラスにあるtype_castメソッドだ。日付型のカラムの変換はstring_to_timeメソッドで行われている(以下はactiverecord-1.15.5/lib/active_record/connection_adapters/abstract/schema_definitions.rb のコード)。

      def self.string_to_time(string)
        return string unless string.is_a?(String)
        time_hash = Date._parse(string)
        time_hash[:sec_fraction] = microseconds(time_hash)
        time_array = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
        # treat 0000-00-00 00:00:00 as nil
        Time.send(Base.default_timezone, *time_array) rescue DateTime.new(*time_array[0..5]) rescue nil
      end

やはりTimeクラスのインスタンスの生成に失敗した場合にDateTimeで再チャレンジするようになっている。この挙動は最新の2.2.2でも同じなので(コードはだいぶ違う)、仕様ということでいいのだろう。だいぶ罠っぽいが。ということは日時を比較する場合は片方がTimeでもDateTimeでも大丈夫なように書かなければならないわけだが、それってかなり面倒そう。

01月18日(日)

最も危険なプログラミングエラーTop 25

カテゴリ: セキュリティ このエントリーを含むはてなブックマーク はてなブックマーク - 最も危険なプログラミングエラーTop 25

2009 CWE/SANS Top 25 Most Dangerous Programming Errors」というものが公表された。日本の同種のものだと「パラメータ改竄(に対する脆弱性)」みたいに脆弱性と攻撃手法を厳密に分けずにごっちゃになっていることが多いが、このリストは厳密に脆弱性というか、原因となったものについて書いているのが興味深い。せっかくなので訳してみた。表現が端的なので、詳細を読まないとなんのことなのか分からないものもある。

  • コンポーネント間の安全でない相互作用
    • CWE-20: 不適切な入力検証
    • CWE-116: 出力の不適当なエンコーディングまたはエスケープ
    • CWE-89: SQLクエリの構造を保つことに関する怠り(別名SQLインジェクション)
    • CWE-79: ウェブページの構造を保つことに関する怠り(別名クロスサイトスクリプティング)
    • CWE-78: OSコマンドの構造を保つことに関する怠り(別名OSコマンドインジェクション)
    • CWE-319: 機密情報の平文送出
    • CWE-352: クロスサイトリクエストフォージェリー(CSRF)
    • CWE-362: 競合状態
    • CWE-209: エラーメッセージによる情報漏洩
  • 危険なリソース管理
    • CWE-119: バッファメモリの境界内に操作を制限することに関する怠り
    • CWE-642: クリティカルな状態データの外部制御
    • CWE-73: ファイル名やパスの外部制御
    • CWE-426: 信頼できない検索パス
    • CWE-94: コードの動的生成の制御に関する怠り(別名コードインジェクション)
    • CWE-494: 改竄チェックの無いコードダウンロード
    • CWE-404: リソースの不適切なシャットダウンや解放
    • CWE-665: 不適切な初期化
    • CWE-682: 誤った計算
  • 穴だらけの防御
    • CWE-285: 不適切なアクセス制御(認証)
    • CWE-327: 破られている、または危険な暗号化アルゴリズムの使用
    • CWE-259: パスワードのハードコード
    • CWE-732: クリティカルなリソースに対する安全でない権限割り当て
    • CWE-330: 能力不足の乱数の使用
    • CWE-250: 不必要な特権での実行
    • CWE-602: サーバ側セキュリティのクライアント側施行

インジェクション系は「構造を保つことに関する怠り(Failure to Preserve ... Structure)」という表現になっている。確かに、プログラミングエラーとして厳密に表現するとそういうことになるのだろう。実装に問題があるということが分かるのでなかなかいいと思う。

「クリティカルな状態データの外部制御(External Control of Critical State Data)」は何のことだが分かりづらいが、セッションデータなど、改竄されてはいけない状態情報をhiddenやクッキーなどに保存することで外部から受け取る状態になってしまっていることを示している。「ファイル名やパスの外部制御」も同様で、要はディレクトリトラバーサルに弱いという問題である。

「サーバ側セキュリティのクライアント側施行Client-Side Enforcement of Server-Side Security」というのは、本来サーバ側で行わなければならない検証や認証をクライアント側だけで行っているので、いくらでもスルーできるというものだ。これはAJAXやFLASHなどで良く起る。

CSRFがそのままなのが残念といえば残念。「Failure to ...」の形にするとしたらどういう文になるだろうか。

01月13日(火)

開発者視点の脆弱性の分類

カテゴリ: セキュリティ このエントリーを含むはてなブックマーク はてなブックマーク - 開発者視点の脆弱性の分類

Webアプリの脆弱性は5+1の分類で把握せよ-NTTデータCCS長谷川氏より

そこで同氏が提唱するのが、侵害パターンによる「5+1の類型」だ。Webアプリケーションの侵害パターンは、その脆弱性の性質によって異なる。同氏はそのパターンから脆弱性を「暴露問題」「エコーバック問題」「入力問題」「セッション問題」「アクセス制御問題」の5種類と、それらどれにも含めることのできない「各種の問題」に分類し、「こうすることで脆弱性を体系的に把握することが可能になり、対策の検討もしやすくなる」としているのだ。

この分類は既存のソフトウェアに対する脆弱性検査を行う際には有用だと言えるが、実際の開発の際には不十分だと思う。記事中にあるようにソフトウェア開発は、要件定義、設計、実装と段階を追って進んで行く訳だが、その各段階で具体的に何に対して注意しなければならないかは、この分類だけでは分からない。結局、全工程において危なそうなものを気合いで見分けて対処しるという状況は変わらないわけで、これで何かが劇的に変わる訳ではない。実際、この分類を使えば即セキュアなウェブアプリケーションを作れると思った開発者はどれくらい居るだろうか。つまり、この分類はセキュリティ検査屋の為の分類であって、開発者のための分類ではないのだ。

では開発者のための脆弱性とはどういったものだろうか。ソフトウェアにまつわる問題というのは設計・実装・運用のいずれかが原因になっている。どんなに完璧に実装したとしても、そもそもの設計や後の運用が悪ければ駄目だし、設計や運用が完璧でも、バグがあったらどうしようもない。脆弱性もソフトウェアにまつわる問題には違いないので、この3つに分類できるはずだ。ということで開発者視点の脆弱性としては、まず以下の3つに分類したい。

  1. 設計上の不備
  2. バグの副作用による脆弱性
  3. 運用上の不備

たとえばXSS。これはHTMLのメタ文字の不適切な取り扱いが原因のバグが原因で、CSVファイルでカンマを特別扱いし忘れたのと同じレベルのバグである。バグの主な症状は表示が崩れるというものだが、それを利用してscipt要素やスクリプトが実行可能な属性と認識されるような表示の崩れを起こさせることが可能であれば、他人のサイトで任意のスクリプトを実行させることができるという脆弱性になる。XSSはバグが原因であるため、設計段階で対処するのは難しい。もちろん、設計段階で何がしかの配慮をすることは可能だが、それに対する検証をテスト工程に盛り込まなければ結局発生は抑えられずに終わる。バグの発生に対する有効な対策はテストしかないので、一通り動作する事だけではなく、誤動作しないというテストを行うことが肝心である。それは難しいことではなく、HTMLのメタ文字である '"<>& といった文字を入力して問題無く扱えるか確認するだけといったレベルのものである。

対してCSRFは設計レベルの脆弱性である。ウェブアプリケーションとしては問題無く動作しているものだが、その機能の設計自体に脆弱な隙があったというものだ。どんなに完璧に実装して完璧にテストしたとしても、そもそもの設計が脆弱なのでは意味がない。設計に脆弱性があると後の工程から大きな手戻りが発生したり、リリース後の修正が難しいものになるため、設計段階で考慮し修正しておくことが肝心である。ある意味、このレベルからが真の脆弱性と言えるのではないだろうか。開発者的には前者のXSS等はただのバグなわけで、単に直せばいいものである。セキュリティ対策が----などといった大層なものではない。

運用上の不備としては、クリティカルなファイルを公開ディレクトリに置いてしまった、認証をかけ忘れた、パスワードが脆弱などの問題が挙げられる。この辺はもう、どんなにアプリケーションの設計やテストを頑張ったとしても防げない。それらとは別の、危険な運用をさせない運用設計とテストが必要になる。

開発期間が短くなりがちなウェブアプリケーションではテスト工程が簡略化され、正しく動作する事だけがテストされ、誤動作しないというテストは行われない事が多い。設計上の不備に関してはそれを補ってくれるフレームワーク等を利用することで改善が期待できるが、バグに関しては自分たちでテストするしかない。XSS、SQLインジェクション、OSコマンドインジェクションなど、脆弱性の大半を占めるものはメタ文字の不適切な取り扱いというバグの副作用による脆弱である。したがって、メタ文字を入力して正しく扱えるか確認するというテストを追加するだけで、セキュアなアプリケーションになることは間違いないだろう。