Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

SQLite2と3のエスケープ関数の違いとその対策

7,299 views

Published on

SQLite3::escapeString()がバイナリセーフではないのが正解である理由を、SQLite2と3の本体とPHP側の対応の歴史を踏まえながら見ていきます。

Published in: Engineering
  • Be the first to comment

SQLite2と3のエスケープ関数の違いとその対策

  1. 1. SQLite2と3のエスケープ 関数の違いとその対策 第六回闇PHP勉強会 2015-11-22 @noldorinfo
  2. 2. 自己紹介 • 竹腰彰成(たけこしあきしげ) • Twitter: @noldorinfo • http://blog.noldor.info/ • 作ったもの:KinoWiki(2005年) • 10年前、SQLite2で作ったものです。 • CentOS 7にジャンプアップしたら動かなくなったという連絡を受けて SQLite3に移行できるように修正しました。 • 案外と手間がかかりました。SQLite2と3に互換性が一部ありません。 • というわけで、今回はその話をします。 2
  3. 3. 今日のテーマ • SQLite2と3のエスケープ関数の違い • SQLite2はバイナリセーフ、3はバイナリセーフではない • どうしてそうなったか • SQLite2はネイティブがバイナリ非対応でPHPで頑張っちゃった • SQLite3はネイティブでバイナリ対応なのでPHPで頑張らなかった • そのためPHPとしては互換関数を用意しにくい • 使う側としての対応の仕方 • 自前で互換関数を作成する 3 エスケープではなく プレースホルダ使えという 話は横に置かせてください
  4. 4. ネイティブのSQLite2と3 DBとしてのバイナリの取り扱い 4
  5. 5. SQLite2 • 組み込み型RDBMS • 1ファイル1データベース • データ型を指定する必要がない • データはすべてC言語の文字列(=nullターミネートのchar配列) • PHPの暗黙型キャストと非常に親和性が高い • 数字の文字列なら数値とみなす、など • Int/Real/Text/BLOBなどを指定可能だが無視している • BLOBに0は入らない(nullターミネータとして判定される) • バイナリはsqlite_encode_binary()で’0’をエンコードしてから保 存、読み込み時にsqlite_decode_binary()でデコードするルール 5
  6. 6. SQLite3 • 型が増えた • null/Integer/Real/Text/BLOB • IntegerをIntegerとして取り扱うなどで高速化 • SQLite2での足し算などは文字列をIntegerに変換していた • BLOBもある • リテラル表現で0が表現可能になった • X’FFFFFF……’ • 文字列の前にXを置いて16進表記 • 0は「X’00’」 6
  7. 7. SQLite2と3でバイナリの扱いが変わった • SQLite2ではすべて“C言語の文字列” • C言語の文字列=nullターミネートのchar配列 • バイナリはsqlite_encode_binary()で’0’をエンコードしてから保存、 読み込み時にsqlite_decode_binary()でデコードするルール • SQLite3ではBLOB型が加わった • リテラル表現は「X’FFFFFF……’」 • sqlite_encode_string() / sqlite_decode_binary()は廃止 • 作者曰く「ネイティブで対応したからもう要らないよね」 http://sqlite.1065341.n5.nabble.com/Is-it-mandatory-to- use-sqlite-encode-binary-amp-sqlite-decode-binary-to-store- data-structures-imagess-td63311.html 7
  8. 8. PHPのSQLite2と3 バイナリの取り扱いに互換性がなくなった 8
  9. 9. PHPのSQLite3のエスケープ関数 • SQLite3と同時期にPDOができ、SQLite3はPDOでの提供と なった • 命名ルール的にはsqlite_escape_string()の代わりが SQLite3::escapeString()のように見える • でもバイナリセーフじゃない • バグ報告がある • https://bugs.php.net/bug.php?id=63419 • https://bugs.php.net/bug.php?id=62361 • ともに未解決 • 「PHPのバグじゃないよ、SQLite3のmprintf(“%q”)呼んでるだけ だよ」 9
  10. 10. SQLite2/3のmprintf(“%q”) • The %q option works like %s in that it substitutes a nul- terminated string from the argument list. But %q also doubles every ''' character. %q is designed for use inside a string literal. By doubling each ''' character it escapes that character and allows it to be inserted into the string. • nullターミネートの文字列を受け取って、シングルクォートを2 つ重ねる(SQL向けにエスケープする) • ということは、PHPの文字列に’0’があるとそこで途切れる • これがバイナリセーフではない正体 10
  11. 11. SQLite2のsqlite_encode_binary() • http://www.sqlite.org/cgi/src/artifact/fc8c51f0b61bc803 1. 「適切な数値」e を選ぶ(エスケープ結果が小さくなるよう調整) 2. 文字列のすべてのバイトに対して数値eを引く(ただし8ビットなの でmod 256) 3. 次のルールで各バイトを置換する。 • x00 -> x01 x01 • x01 -> x01 x02 • x27 -> x01 x28 4. 置換済み文字列の前に数値eを付与してreturnする 11 0、x27(シングルクォート) が文字列から無くなる
  12. 12. PHPのsqlite_escape_string() • PHPのsqlite_escape_string()はネイティブの sqlite_encode_binary()を呼び出しているだけじゃない 1. 空文字列なら空文字列を返す(※ガード条件) 2. 「先頭がx01」または「0を含んでいる」なら sqlite_encode_binary()を呼び出し、先頭にx01を付与して返す 3. ‘0’がなければmprintf(“%q”)を呼び出す • 役割が2つある • バイナリのエンコード • 文字列のエスケープ 12
  13. 13. sqlite_escape_string()の役割 バイナリのエンコード 文字列のエスケープ SQLite2 sqlite_enscape_string() 実体はsqlite_encode_binary() とフラグ立て(先頭にx01) sqlite_escape_string() 実体はmprintf(“%q”) SQLite3 なし(リテラル表現可能) SQLite3::escapeString() 実体はmprintf(“%q”) 13 SQLite3::escapeString()はsqlite_escape_string()の代わりではない
  14. 14. PHPで実装 C言語を読んでPHPに書き換える 14
  15. 15. バイナリのエンコード関数を作る • ないものは作ればいい • sqlite_escape_string()との互換性を持つ関数があれば置き換え可能 • (本当はプレースホルダにしたいところだけど……レガシーコードゆ え許されたし) • リテラル表現では互換性を取れない • X’FFFFFF……’ だがSQLite2を使うPHPコードはシングルクォートの 外にXを置く想定をしていない • エンコードフラグx01はPHP側で実装していたのでSQLite側は無関与 • sqlite_escape_string()をPHPで再実装するのがてっとり早い • それぞれの関数はコード量が非常に小さい • SQLiteはコメントたっぷり 15
  16. 16. sqlite_escape_string():33行 • https://github.com/php/php-src/blob/PHP- 5.3.29/ext/sqlite/sqlite.c#L3153 16 PHP_FUNCTION(sqlite_escape_string) { char *string = NULL; int stringlen; char *ret; if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &string, &stringlen)) { return; } if (stringlen && (string[0] == 'x01' || memchr(string, '0', stringlen) != NULL)) { /* binary string */ int enclen; ret = safe_emalloc(1 + stringlen / 254, 257, 3); ret[0] = 'x01'; enclen = php_sqlite_encode_binary(string, stringlen, ret+1); RETVAL_STRINGL(ret, enclen+1, 0); } else if (stringlen) { ret = sqlite_mprintf("%q", string); if (ret) { RETVAL_STRING(ret, 1); sqlite_freemem(ret); } } else { RETURN_EMPTY_STRING(); } }
  17. 17. sqlite_encode_binary():41行 • http://www.sqlite.org/cgi/src/artifact/fc8c51f0b61bc803 17 int sqlite_encode_binary(const unsigned char *in, int n, unsigned char *out){ int i, j, e, m; unsigned char x; int cnt[256]; if( n<=0 ){ if( out ){ out[0] = 'x'; out[1] = 0; } return 1; } memset(cnt, 0, sizeof(cnt)); for(i=n-1; i>=0; i--){ cnt[in[i]]++; } m = n; for(i=1; i<256; i++){ int sum; if( i==''' ) continue; sum = cnt[i] + cnt[(i+1)&0xff] + cnt[(i+''')&0xff]; if( sum<m ){ m = sum; e = i; if( m==0 ) break; } } if( out==0 ){ return n+m+1; } out[0] = e; j = 1; for(i=0; i<n; i++){ x = in[i] - e; if( x==0 || x==1 || x=='''){ out[j++] = 1; x++; } out[j++] = x; } out[j] = 0; assert( j==n+m+1 ); return j; }
  18. 18. sqlite_decode_binary():13行 • http://www.sqlite.org/cgi/src/artifact/fc8c51f0b61bc803 18 int sqlite_decode_binary(const unsigned char *in, unsigned char *out){ int i, e; unsigned char c; e = *(in++); i = 0; while( (c = *(in++))!=0 ){ if( c==1 ){ c = *(in++) - 1; } out[i++] = c + e; } return i; }
  19. 19. PHPでの実装 • unsigned char配列操作のPHP実装は次の点がポイント • ord():文字列の先頭1文字からASCIIコードを返す(string→int) • chr():ASCIIコードから1文字の文字列を返す(int→string) • 文字列への[]演算子でchar配列としてのアクセスが可能 • 「文字列への文字単位のアクセスと修正」 http://php.net/manual/ja/language.types.string.php#langua ge.types.string.substr • ビット演算子&で1バイトにマスクする • 実装しました • https://github.com/noldor/kino2/commit/8059e70df9d592305f 7d6a1a5e1364b9e49a043a 19
  20. 20. 残った難点 • SQLite2と3でバイナリデータの互換性がない • SQLiteそのものはSQLダンプで移行可能 • $ sqlite OLD.DB .dump | sqlite3 NEW.DB • エンコードフラグx01はPHP側で勝手に用意したオレオレルール • 自前実装版もx01を勝手に使うオレオレルール • エンコードするとsqlite3コマンドで意味が通らなくなる • そのためPHP本体では互換関数を用意しにくい 20
  21. 21. 今日のまとめ • SQLite2と3のエスケープ関数の違い • SQLite2はバイナリセーフ、3はバイナリセーフではない • どうしてそうなったか • SQLite2はネイティブがバイナリ非対応でPHPで頑張っちゃった • SQLite3はネイティブでバイナリ対応なのでPHPで頑張らなかった • そのためPHPとしては互換関数を用意しにくい • 使う側としての対応の仕方 • 自前で互換関数を作成する 21
  22. 22. ご清聴ありがとうございました 22

×