Hatena::ブログ(Diary)

qsonaの日記

QLOOKアクセス解析

2014-04-06

uncaughtExceptionのハンドラの中で例外発生させたときの挙動

基本的には、プログラムで例外が発生したときに、それがcatchされないとプログラムは終了する(落ちる)。
しかし、実際に運用する環境のプログラムでは、仮にcatchされなかったとしてもプログラムを止めることはできないので、
どうにかする。

Node.jsの場合は次のように、プログラム中でcatchされなかった例外を最も低レベルで捕捉することができる。

process.on('uncaughtException', function(err) {
    console.log('an error occured', err);
});

processはNode.js上ではグローバル変数である。
なお注意として、この方法は現在推奨されておらず、将来削除される可能性があるとのこと。

Note that uncaughtException is a very crude mechanism for exception handling and may be removed in the future.

Don't use it, use domains instead. If you do use it, restart your application after every unhandled exception!

http://nodejs.org/api/process.html#process_event_uncaughtexception

uncaughtExceptionをハンドルするコールバック関数内で、uncaughtなExceptionを発生させたらどうなるか


無限ループが起こりそうな気がするが、
しかし例えば来たものをそのまま投げるだけのときとかもあるので、無限ループされても困る気がする。

実際に試してみた。

process.on('uncaughtException', function(err) {
    console.log('an error occured', err);
    throw err;
});
throw new Error('foo'); // トリガを引く

結果

an error occured [Error: foo]

            throw err;
                  ^
Error: foo
    at Object.<anonymous> (/foo/bar/uncaught_exception_test/server.js:4:15)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:902:3

uncaughtExceptionのハンドラ内で単純に例外をスローした場合、その例外はもはや捕捉されることなく、プログラムは終了するということがわかった。

それでは、単純にスローせず、非同期関数を1枚かぶせるとどうなるか

process.on('uncaughtException', function(err) {
    console.log('an error occured', err);
    // 次の行は非同期関数ならなんでもいい
    fs.stat('./app.js', function() {
        throw err;
    });
});
throw new Error('foo');

結果

an error occured [Error: foo]
an error occured [Error: foo]
an error occured [Error: foo]
an error occured [Error: foo]
an error occured [Error: foo]
...

というわけで、めでたく無限ループになった。(当たり前といえば当たり前)
したがって、このハンドラ内で外部への通知などちょっと凝ったことをする場合は、注意しなければならない。

この辺の挙動はおそらく、Node.jsのイベントループについてもっと詳しく知っていれば調べるまでもないのだろうから、
きちんと知っておかないとなぁ。

2014-03-24

とりいそぎExpress4を試す。

Expressを使い始めるためにひな形を作成するexpress-generatorが、Express4に対応してたので
とりあえずそれを使って試すのがよさそう。

本来なら

npm install -g express-generator

ですが、まだnpmにはExpress4向けのものは上がっていないので、githubから直接cloneしましょう

# どっか適当な場所で
git clone git@github.com:expressjs/generator.git
cd ./generator
npm install
cd ..
express express4test

express4testフォルダが作成されて、中にひな型が入ってます。

中身を見ると、例えばこんな感じでexpress4仕様になってます。

// app.js
// Express4からこの辺のモジュールはExpressに含まれなくなった
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

あと、Routerの使い方も見れます。正直いまいち分かってないので後でちゃんと見よう。

2014-03-16

バリデーションとエラー処理について

値の妥当性チェックはどこでやるべきなのかの話

Webアプリケーションに限った話ではありませんが、特にWebアプリケーションを作っているとき、
ユーザから受け取った値のバリデーションは必ずどこかで行わなければなりません。

例えばJavaでモデルを作るとき、ロジックが入ってくるメソッド等では最初に引数チェックを入れるのが普通ではないでしょうか。
次はトランプのカードを作成するコンストラクタの例です。

public enum Suit { /* 略 */ }
public Card(Suit suit, int num) {
    if (num < 1 || 13 < num) {
        throw new IllegalArgumentException("カードの数値は1から13までで指定しる");
    }
    this.num = num;
    // 略
}

上の例では、Cardモデルのnumに変な値が紛れ込むことは絶対にない、ということになります。

Cardを作成するときのコードはこんな感じだったとしましょう。

List<Card> deck = new LinkedList<>();
for (Suit suit in Suit.values()) {
    for (int num = 1; num <= 13; num++) {
            deck.add(new Card(suit, num));
    }
}

このとき、外部からの入力に頼っていないので当たり前ではありますが、
Cardコンストラクタ引数チェックにはひっかかりません。
もしCardを作成する場所がここだけだったとしたら、引数チェックはなくてもよい、無駄な処理ということになります。

だとしても、上のコンストラクタ引数チェックは不要だからないほうがよい、という人はあまり居ないと思います。それがJavaの思想だと自分は理解しています。

一方でJavaScriptでは、上のような引数チェックはむしろ省略されることの方が多いのではないでしょうか。

JavaScriptは動的型付けであることから、いちいち引数に対してちゃんとチェックしようとすると、その型から調べていかなくてはいかなかったりして非常に大変です。

function Card(suit, num) {
    // 数値である、整数である、範囲内である、をチェックしている
    if (!Number.isFinite(num) || num !== (num | 0) || num < 1 || 13 < num) {
        throw new Error('カードのnumは1から13までの整数で指定しる');
    }
    this.num = num;
}

一概には言えませんが上のようなコードは、ちょっとJavaScriptとしては筋が悪い感じがします。
自分もJavaしか書いた事がなくて、初めてJavaScriptを書き始めたときは上みたいなコードを書いてました。

で、じゃあどうするかというと、基本的なチェックはコントローラ側(呼び出し元)でするということになります。

Webアプリでユーザから送られてきた値を利用するときは、基本文字列ですから
(前回書いたように、Node.jsのExpressでreq.body.xxxやreq.query.yyyの値は文字列ではなくオブジェクトになっていることがあるため、注意が必要ですが)
文字列を数値に変換したりするときに、諸々チェックすることになります。

ここのところJavaだと、いったんString型で受け取ったのをintに変換するところまではコントローラ側の責務で、そこから先の範囲チェックとかはサービス層で処理とかになりそうです。
この辺の処理がめんどくさくて、個人レベルのWebアプリJavaで書くのはやっぱりだるいなあという感じになりますがそれは余談として。

JavaだろうとJavaScriptだろうと共通なこととして、
そのモデルにかなり密接に紐づくエラーに関しては、事前にチェックするよりも、モデルに値を渡したあとでチェックしたほうが効率的です。

例えばオセロで、白番の人があるマス目(例えばe2)に置く、という入力が送られてくることを考えます。
このときチェックしなければならない項目は次のようなものがあるでしょう。

  1. マス目は正しい値か。(k9のように存在しないマスだったら弾く)
  2. 手番が正しいか。
  3. そのマスに、すでに石が置いてないかどうか。
  4. そのマスに置くことで、1つ以上石をひっくり返せるかどうか。

この項目のうち、1はオセロというゲーム自体がもつ情報からチェックでき、
2-4は、このオセロインスタンスに紐づく情報を参照しなければチェックできません。

では、これらのチェックはそれぞれ、置くメソッドを呼ぶ前と呼ぶ後どちらで処理すべきでしょうか?

まず可能かどうかを考えたとき、1は、置くメソッドを呼ぶ前にチェックすることが可能です。オセロプログラムを作っているのだから、オセロのゲーム自体の情報はプログラム内どこからでも参照できなければおかしいからです。

2,3に関しては、オセロモデルのインスタンスに紐づく情報が必要であるので、オセロモデルがこれらの情報を隠蔽していなければ可能ですが、
「現在の手番」「盤面の状況」という情報はユーザにとっても必須であるから隠蔽されることは考えにくいので、やはり呼ぶ前にチェックできます。

では、4はどうでしょうか。
「石をひっくり返せるかどうかのチェック」は、実際に「石をひっくり返す」という処理と密接に関係しています。
チェックに使う情報は盤面の状況だけですから、外からでもチェックできないことはありませんが、
ひっくり返すのをチェックして、終わってからひっくり返す、とすると二度手間になること間違いなしでしょう。

したがってこういうものに関しては、JavaであろうとJavaScriptであろうと、モデル側で処理をするのが自然ということになります。
ひっくり返す処理を実際に行うメソッドの中でチェックを行い、ダメだった場合はエラーを返すわけです。

私なら、Javaなら1-4全てモデル側でチェック、JavaScriptなら1を呼び出し元・2-4をモデル側でチェックすると思います。

また、「エラーを返す」という微妙に曖昧な書き方をしましたので、その方法について書くと、
Javaを想定すると、この関数の場合、成功時に特に返さなければいけないデータがあるわけではないので、
成功 または 失敗理由 のenumを用意し、それを返すような実装に自分ならします。

もちろん例外を使っても良いのですが、失敗理由全ての例外クラスを用意しなければならないため、ちょっと仰々しい感じがします。

Node.jsでのエラー処理について

Node.jsにおいて非同期なメソッドをつくるとき、守るべき規約があります。

function anAsyncMethod(foo, bar, callback) {
  var baz = syncMethod(foo, bar);
  anotherAsyncMethod(baz, function(err, result) {
    if (err) {
      // エラーがあるときは第一引数にErrorオブジェクトを渡す
      return callback(err);
    }
    // 正常終了のときは第一引数nullでコールバックを呼ぶ
    callback(null, result);
  });
}

したがって、非同期なメソッドの中でエラーが発生する可能性がある場合は、この規約を守ればよいので特に問題はありません。

問題は、同期メソッドの時にどうするか。

自分は次の3通りが考えられると思います。(他にあったら教えてほしいです。)

  1. 例外機構(throw / try-catch)を使う。
  2. Errorオブジェクトをreturnする。
  3. 同期メソッドも非同期メソッドと同じ書き方にする。

このうち、3は、場合によってはありえると思います。
具体的には、処理が重い関数などは、本来なら同期処理ですむ場合でもnextTickやsetImmediateを挟んで一旦他の人に順番を渡す、ということをすべきになる可能性があります。
nextTickやsetImmediateを挟んだ瞬間、非同期関数になりますから、
そうなった場合に、呼び出し元の書き方を変える必要がないのは一つのメリットです。

しかしながら、そういうことが考えにくい場合は、同期処理なのに非同期のように書くのは混乱を生じさせるだけと思いますので、少なくともプロジェクト全体を3で統一するというのは考えにくいです。

それでは、1と2の書き方の違いを見てみましょう。

// オセロモデル側
// 石を置く
Othello.prototype.put = function(teban, x, y) {
  if (this.teban !== teban) {
    throw new Error('手番ではありません');
  }
};

// 呼び出し側
try {
  othelloInstance.put('white', 2, 3);
} catch(e) {
  // エラー処理
}

// オセロモデル側
// 石を置く
Othello.prototype.put = function(teban, x, y) {
  if (this.teban !== teban) {
    return new Error('手番ではありません');
  }
};

// 呼び出し側
var result = othelloInstance.put('white', 2, 3);
if (result instanceof Error) {
  // エラー処理
}

見た目的にどっちが良いかは好みとしか言いようがないかと思います。
タイプ数も大してかわりません。

なので、今回は速度を計測してみました。

var error = new Error('foo');
function method1() {
    throw error;
}
function method2() {
    return error;
}

console.time('a');
for (var i = 0; i < 1000000; i++) {
    try {
        var result = method1();
    } catch(e) {
    }
}
console.timeEnd('a');

console.time('b');
for (var i = 0; i < 1000000; i++) {
    var result = method2();
    if (result instanceof Error) {
    }
}
console.timeEnd('b');

結果

a: 152ms
b: 16ms

というわけで、例外を使うほうが10倍遅いという結果になりました。

なお、今回はnew Errorせずに同じエラーオブジェクトを使い回していますが、
new Errorしたところそれだけで(なにも処理せずとも)5600msかかりましたので、
まあ極力newするなというのもそうですし、
他の処理の重さに比べたらtry-catchにかかる時間など誤差といって差し支えないかもしれません。

例外機構は、catchしなければそのまま上にthrowされるので、その仕組みを有効に使えるのなら、特に使うべきであると思います。
しかしJavaScriptの場合、Javaと違い、try-catchしないと文法エラーとかはないので、2段階以上に例外が伝播するのを正しく運用するのは結構難しい気もしてしまいます。
(Javaの場合、try-catchで囲むか、メソッド宣言にthrowsをつけないとエラーになります。ただし例外がRuntimeExceptionおよびそのサブクラスである場合は除く)

その他では、1にするべき理由としては、標準APIで例外を利用している場合もあるので(JSON.parseなど)、それを使ったりそれに近いことをする場合は、合わせた方が良いとも思います。

また、2の方ですが、
計測した感じでは、Node.jsでのinstanceofは十分速いという結論でよさそうなのですが、それでも気になる場合、あるいはinstanceofなんて長くて書きたくない場合、ErrorオブジェクトプロトタイプにisErrorプロパティをくっつけて、それをtrueにしておけば、

// result instanceof Errorのかわりに
if (result.isError) {
}

と書く事が出来ます。しかし、resultがnullやundefinedである可能性がある場合、result && result.isErrorと書かなくてはなりませんので、これを考えると別にタイプ数が減るわけでもないので善悪は微妙かなという感じがします。

2014-03-13

Webアプリケーションに対する攻撃手法まとめ(0.5 MongoDB インジェクション)

0.5 MongoDB injection

前回の記事でNode.js+MongoDBなら普通は脆弱性出ないから気にしなくていいですよね、等と書いたのですが、
僕自身が今までフツーに脆弱なコードを書いていました・・・。まずはその話から。

概要

Node.js+MongoDBMongoDBドライバを使った時、「SQL文」のように文字列でクエリを投げる事はありません。普通にJavaScriptの文法として、クエリを書く事ができます。
これはすなわち、エスケープ処理のようなものは必要なくなり、
たとえばある文字列で検索するようなクエリを投げるとき、外部からどんな変な文字列を受け取ろうとも、そのままその文字列で検索できる、ということです。

そう、文字列ならね………

脆弱性のある状況

Node.jsはそれがそもそもWebサーバとして動くのですが、
普通は、最低でもExpressという薄いフレームワークをかぶせます。(最近はExpressより厚いフレームワークも出てるみたいですが)
Expressでのコントローラメソッドは、第一引数にRequestをとり(普通、仮引数名はreqとします)、送られてきたデータは以下のように取得できます。

// GETの例
app.get('/', function(req, res) {
  // GETのパラメータはreq.queryに入る
  console.log(req.query);
});
// POSTの例
app.post('/', function(req, res) {
  // POSTのパラメータはreq.bodyに入る
  console.log(req.body);
});

それでは実際に、/loginにPOSTで name, passのパラメータを送って、ログインをするコードを書いてみます。
途中のdbアクセスは、node-mongodb-nativeを使っているつもりです。

// ログイン済の人のみ / を許可。まだの人は/loginにリダイレクト
app.get('/', function(req, res) {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  res.render('index'); // htmlを送る処理
});

// ログイン
// 成功したら / にリダイレクトする
app.post('/login', function(req, res) {
  if (!req.body.name || !req.body.pass) {
    // 必要なパラメータがないため、再度ログイン画面へ
    return res.redirect('/login);
  }
  // このname, passをもつユーザがいるか検索。
  // いればそのユーザでログイン成功。いなければログイン失敗、再度ログイン画面へ。
  db.collection('users').findOne({ name: req.body.name, pass: req.body.pass }, function(err, user) {
    if (err) {
      return res.redirect('/error');
    }
    if (!user) {
      // nameとpassが一致したユーザがいなければ、ログイン不可
      return res.redirect('/login');
    }
    // ログイン成功!
    req.session.user = user;
    res.redirect('/');
  });
});

長々と書きましたが要はここです。

db.collection('users').findOne({ name: req.body.name, pass: req.body.pass }, function(err, user) {

POSTで送られてきたパラメータnameとpassを利用して、ユーザを1件取得するだけです。
先も書いたように、パラメータの値にどんな文字列が来ても、その文字列が値として入るだけなので問題なく動くように見えます。

攻撃手法

MongoDBクエリでは、次のように、値にオブジェクトを指定することができます。

{ age: { $gt: 25 } } // 25歳より上
{ type: { $ne: 'normal' } } // typeがnormalではないもの 

このようにしてMongoDBは柔軟なクエリを表現することができます。

一方、req.paramやreq.bodyは、送られ方によって中がオブジェクトに展開されます。
例えばリクエストのボディを

name[foo]=1234&name[bar]=5678&pass[baz]=90

のようにすれば、サーバ側でreq.bodyは

{
  name: {
    foo: '1234',
    bar: '5678',
  },
  pass: { baz: '90' }
}

のようにオブジェクトに直してくれるわけです。このオブジェクトに関しては任意のkey・valueの組み合わせを送る事が出来ます。

それでは、req.bodyが以下のような形になっていたらどうでしょうか?

{ name: { $ne: 'どんなユーザとも一致しないユーザ名' }, pass: { $ne: '一致しないぱすわーど' } }

neはnot equalですから、この条件でfindすると、全員と一致することになります。
今回はfindOneなので、誰かしら1人が選ばれ、そのユーザとしてログイン(なりすまし)が可能となってしまうのです。。

つまり、req.body.nameやreq.body.passにオブジェクトが来た場合、それをチェックせずに通過されてしまうとインジェクションになるのです。

これは気付きにくい問題でした。SQLと違って文字列でクエリを組み立てているわけでもなく、クエリ自体がjsオブジェクトだから安全と思いきや、
さらに中身がオブジェクトで来られると困るとは・・・。

対策

この対策はとりあえず簡単で、文字列であることを確認すればよいだけです(typeof req.body.name === 'string' でとりあえず良いでしょうか)。

しかし、毎度typeofとか_.isStringするのはそれなりに面倒だし、普通に忘れそうですよね…。
うーん、なんか良い方法ないでしょうか?忘れずにやるしかないのかな…

Webアプリケーションに対する攻撃手法まとめ(0.SQLインジェクション)


前置き

会社で1ヶ月ほど技術研修を受けましたので、その整理をするつもりで記事を書き始めましたが、
文にしようとすると自分の分かっていないところが諸々出てくるため、結局深彫りして調べる必要があり、結果書くのに要する時間も文量も予定の4倍くらいになっています・・。まだまだ沢山勉強しなければならないなと感じています。
主目的は自分の勉強ですが、なるべく間違った事を世に発信しないよう心がけております。
最初に1度だけ言い訳をすると、業務で実際に諸々の問題に直面したことがまだない(まだそのレベルに達していない)ため、自分の考えが及んでいないことが多々あると思います(もっと複雑な要件に対してはどうするんだ等)。
ご指摘を真摯に受け止めて、記事自体も正しく修正していきたいと思いますので、ぜひコメントの方に忌憚なくお書きいただけませんでしょうか。よろしくお願いいたします。

0. SQL injection

攻撃可能になる条件

動的にSQL文を作成する、すなわち、外部からの入力をSQL文を埋め込んでいるときに起こり得ます。
例えば、asdfという名前の本をbooksテーブルから検索するSQL文は、例えば次のようになります:

SELECT name, author FROM books WHERE name = 'asdf';

一般的には'asdf'は、例えば検索フォームに入力されて送信されてくるなど、外部から入力される値でしょう。これを、例えばサーバサイドのコードがJavaScriptだったとすれば(Node.jsを想定)、単純に書けばこうなります。

// name はリクエストで受け取った値。(asdf)
// SQL文を動的に生成
var sql = "SELECT name, author FROM books WHERE name = '" + name + "';"
// このSQL文でDBに問い合わせる

そして、こう書いた瞬間にSQL injectionの脆弱性が発生します。

攻撃手法

上の例で、サーバは攻撃者より、nameとして次のような値を受け取りました。

asdf' UNION SELECT name, password FROM users; --

(※最後にスペースが入っています)

すると、合成された結果SQL文はこうなります:

SELECT name, author FROM books WHERE name = 'asdf' UNION SELECT name, password FROM users; -- '

最後の-- はこの行のこれ以降をコメントにする意味で(JavaScriptの//と同じ)、最後の'を消して文法エラーを避けています。
asdfという名前の本とともに、usersテーブル全件のname, passwordを引き出していることになります。
(実際にnameやpasswordというキー名ではないかもしれませんが、この前にテーブル情報を引き出す攻撃を行うことができてしまいます)

対策(結論からいうと)
  • 単にプリペアドステートメントを使え
  • 絶対に文字列結合でSQLを構築しようとしてはいけない
  • IPAの「安全なSQLの呼び出し方」を読むこと
http://d.hatena.ne.jp/ajiyoshi/20100409/1270809525

対策

自前でエスケープ処理を作成するべきではありません。
基本的には、最も優先して採るべき方法は次の1つです:
◎Prepared Statementを使う。

私が普段Node.jsを利用していますので今回はNode.js+MySQLを利用したサンプルを挙げていきます。
ただし、ここで挙げる例は正確にはPrepared Statementではありません。書き方は同じです。これについては後述します。
以下はNode.js+MySQLドライバ(no title)でPrepared Statementモドキを使った例です。

var name = 'asdf'; // 実際は外部から入力される値
// mysqlClientは、mysqlドライバのcreateConnectionで作成されたオブジェクトとする
// 代入したい部分に、 ? を使う
var preparedStatement = 'SELECT name, author FROM books WHERE name = ?;' ;
mysqlClient.query(preparedStatement, [name], function(err, books) {
  if (err) throw err;
  console.log(books); // [{ name: 'asdf', author: 'qsona' }, {name: 'asdf', author: 'someone' }]
});

複数の値を入れたいときは、?を複数使えます。その時は、mysqlClient.queryの第2引数配列に複数入れます(入れる順番通りに)。

注意点1: Prepared Statement自体を文字列連結等で作ってはいけない

Prepared Statementは、SQL文においてリテラル以外を自分の手で作っている(外部の入力が入る余地はない)から安全なわけです。
ですからいくらPrepared Statementを使っていても、それ自体を動的に生成してしまっては、そこにSQLインジェクションが入る余地が生じてしまいます。

注意点2: LIKE検索

リテラルの部分、実際のSQL文では'asdf'とシングルクォートで囲まれるところ、Prepared Statementの文では '?' と書かずに単に ? と書きます。
ここで、LIKE文を使う時には少し問題になりそうです。
すなわち、例えば書名がasdfから始まる本を探したいときのクエリ

SELECT name, author FROM books WHERE name LIKE 'asdf%'

となります。これを実現したいとき、PreparedStatementはどう書けばよいでしょうか?次の2つは両方誤りです:

var wrongPreparedStatement1 = "SELECT name, author FROM books WHERE name LIKE ?%;" ;
var wrongPreparedStatement2 = "SELECT name, author FROM books WHERE name LIKE '?%';" ;

最初の例では ? に 'asdf' が入ったことを考えれば、上の2つは誤りであることが理解できると思います。とりあえず動くサンプルとしては次のようになります。

var name = 'asdf';
var preparedStatement = 'SELECT name, author FROM books WHERE name LIKE ?;' ;
// 入れる文字列の方に % をつける
name2 = name + '%';
mysqlClient.query(preparedStatement, [name2], function(err, books) {
  // 略
});

SQLインジェクション対策とは別の問題として、
この例では書名に%が使われたときに困ります(実際には%の他に_も意味を持ちます)。

var name = 'いちご100%';
name2 = name + '%'; // 'いちご100%%' となってしまう。書名の一部であったはずの%も、ワイルドカードと認識されてしまう

上の例は、'いちご100個'などでもひっかかってしまいます。

というわけで、こういう場合はエスケープが必要になります。
MySQLならデフォルトのエスケープ文字は \ とのことですから、エスケープ必要な文字を単に\から始まるものに置換すればよいでしょう。
(なおSQL文ではエスケープ文字を指定できます。上のSQL文に続けて escape '%'とかけば%がエスケープ文字となり、いちご100%%はいちご100%の完全一致となります。)

補足すると、この方法はエスケープの処理如何に関わらず、安全です。すなわち、エスケープ処理を仮にミスって期待通りでない結果を返すことがあったとしても、SQLインジェクションは発生し得ないということです。
PreparedStatement自体に文字列連結等をしているわけではなく、それに代入するパラメータをいじっているだけだからです。

注意点3: 柔軟な検索

次のような状況はどうでしょうか。
(入力フォーム)
著者名 [ ]
書名 [ ]
価格 [ ] 以上 [ ] 以下
[検索]

この時、先と同じように考えれば次の例のようになります:

var preparedStatement =
  'SELECT * FROM books WHERE author = ? AND name = ? AND price BETWEEN ? AND ?;' ;
mysqlClient.query(preparedStatement, [author, name, price_low, price_high], function(err, books) {
  // ...

しかし、これで良いと考えるのはさすがにナイーブすぎます。
一般的に検索において、著者名の欄を空欄にしたら、著者名が本当に空文字列なものを検索したいわけではなく、なんでも良いという意味なはずです。
ですから、著者名が空なときは、そもそもWHERE句自体が不要になるわけです。

ではどうしたら良いかというと、MySQLエキスパートの方のブログには以下のように記述されています:

そこで、我々は選択を迫られる。プリペアドステートメントの元になる文字列を動的に組み立てるか、プリペアドステートメントを用いずにクエリを動的に組み立てるか、である。

http://nippondanji.blogspot.jp/2013/12/sql.html

これはエキスパートしか採ってはいけない選択肢であって、私のような初心者が採るべき方法ではありません。それを承知で、たとえば前者の例を挙げるなら以下のようになります。

var badPreparedStatement = 'SELECT * FROM books';
var conditionalStatements = [], keys = [];
if (author) {
  conditionalStatements.push('author = ?');
  keys.push(author);
}
if (name) {
  conditionalStatements.push('name = ?');
  keys.push(author);
}
if (price_low) {
  conditionalStatements.push('? <= price');
  keys.push(price_low);
}
if (price_high) {
  conditionalStatements.push('price <= ?');
  keys.push(author);
}
// 各条件をANDで結合して、PreparedStatementを作成
if (keys.length) {
  badPreparedStatement += ' WHERE ' + conditionalStatements.join(' AND');
}
badPreparedStatement += ';' ;

badと書きましたが、おそらくこの段階ではSQLインジェクション脆弱性は発生していないと思いますし、おそらく正しくないSQL分が組み立てられるバグもないと思います。(多分)
しかし、繰り返しになりますがこのような選択肢は最後に考えるべきです。
具体的には、以下のような方法でも実現できないでしょうか。

// price_low, price_highは未入力の場合-1に初期化しておく
var preparedStatement =
  'SELECT * FROM books WHERE (? = '' OR author = ?) AND (? = '' OR name = ?) AND (? = -1 OR ? <= price) AND (? = -1 OR price <= ?);' ;
mysqlClient.query(preparedStatement, [author, author, name, name, price_low, price_low, price_high, price_high], function(err, books) {
// ...略
});

今回は空文字列なら条件指定なし、というふうにしましたが、検索するかどうかのboolean値の変数を別に用意して使っても良いでしょう。
パフォーマンス的には、検証してみないと分かりません。後述する正しい意味でのPreparedStatement(静的プレースホルダ)が使えるなら、キャッシュされるため良いということになると思います。動的プレースホルダの場合は、余計な文字列連結する方法に若干劣るかもしれません(誤差レベルだろうとは思うのですが)。

静的プレースホルダ(PreparedStatement)と動的プレースホルダ

安全なSQLの呼び出し方(pdf)から一部引用します。

静的プレースホルダは、JIS/ISO の規格では「準備された文(Prepared Statement)」と規定されています。これは、プレースホルダのままの SQL 文をデータベースエンジン側にあらかじめ送信して、実行前に、SQL 文の構文解析などの準備をしておく方式です。SQL 実行の段階で、実際のパラメータの値をデータベースエンジン側に送信し、データベースエンジン側がバインド処理します。

http://www.ipa.go.jp/files/000017320.pdf

動的プレースホルダは準備された文(Prepared Statement)とは異なり、プレースホルダを利用するものの、パラメータのバインド処理をデータベースエンジン側で行うのではなく、アプリケーション側のライブラリ内で実行する方式です。

http://www.ipa.go.jp/files/000017320.pdf

そして、静的プレースホルダセキュリティ的に最も安全であり、一方動的プレースホルダライブラリの実装に依っては脆弱性があることを否定できないとしています。

この記事ではNode.jsの例を紹介していましたが、使ったドライバの説明には、プレースホルダの説明の後に次のような記述があります:

This looks similar to prepared statements in MySQL, however it really just uses the same connection.escape() method internally.

MySQLのPrepared Statementと同じ書き方だが、実際にはライブラリの中でエスケープしていますよ、ということです。
ソースを見てみると確かに、ライブラリ内で正規表現を利用して置換処理を行っていました。
すなわちこれは動的プレースホルダであり、書き方こそ同じですがこれはPrepared Statementではないということになります。

他の例を挙げれば、JDBCドライバのPreparedStatementでは、設定で静的プレースホルダか動的プレースホルダか変更できるということです。

本物のPrepared Statment(静的プレースホルダ)を利用する場合、それはそもそもエスケープ処理ですらなく、原理的に安全な方法を採っていることになります。
そうでないものを使う場合、ライブラリをしっかり検証してから使う必要があると思います。

Node.js+MongoDBならこんな気を使う必要ないよね

余談ですが、僕は普段自分の用途ではMongoDBを使っています。MongoDBクエリはそもそもJavaScriptであるため、Node.jsからMongoDBクエリを発行する際はこのような変換が必要ありません。そういった意味では、この組み合わせはラクなだけでなく安全でもありますよね。蛇足感がすごい。

追記

Node.js+MongoDBなら安心とか書いたところ、いやいやそんなことないという突っ込みを頂きました。で、その内容なのですが、僕が普通に見落としていたもので、つまり最近まで僕は普通に脆弱なプログラムを書いていたということで、穴があったら入りたい系です、辛いです。後できちんと書きます…。

参考文献

[SQLインジェクション対策]Webアプリケーションとかの入門本みたいのを書く人への心からのお願い。 - *「ふっかつのじゅもんがちがいます。」withぬこ
安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構より、安全なウェブサイトの作り方、安全なSQLの呼び出し方
漢(オトコ)のコンピュータ道: SQLインジェクション対策に正解はない
java - SQL prepared statement how to select via multiple possible menu selections? - Stack Overflow
他多数のWeb上のブログを参考にいたしました。
ただし、本記事に誤った記述があったとしても、それらは参考文献によるものではありません。
誤りがある場合、コメントにてご指摘頂けると大変有り難く思います。

2014-03-01

express.ioの導入

紹介までしといてまだセットアップもしてなかったのかよっていうツッコミはおいといて。

簡単に紹介し直すと、Node.jsアプリの定番フレームワークとしてExpressがあり、またWebSocketを主に使った双方向通信ができるのがSocket.IOで、
この2つを合わせて使うのに便利なフレームワークexpress.ioです。

で、セットアップで微妙にハマったので、備忘録。といっても1時間弱くらいだから全然大したことないんですがね。

Expressをexpress.ioに置き換えるのは超簡単だよ、と公式にあったので、とりあえずまずExpressのひな形を作成してそれを変更するという手順でやったのですが、最初うまくいかず。
ちゃんとexpress.ioの公式に書いてある書き方にぼちぼち直したら動きました。

手順

今回はasdfprojectという名前にします。

$ npm install -g express
$ express asdfproject -s # -sはセッションを使うときのオプション
# asdfprojectフォルダが作成され、その下にExpressを使い始めるのに必要なファイルが一通りそろっている
$ cd asdfproject

package.jsonを編集。expressをexpress.ioに変えるだけ。

{
  // 前略
  "dependencies": {
    // この行を変更 => "express": "3.4.8",
    "express.io": "*",
    "jade": "*"
  }
}
$ npm install

これでexpress.ioが無事に入ればOK。

app.jsがこんな感じで生成されているはず(express 3.4.8を使用している)


/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

var app = express().http().io();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(express.cookieParser('your secret here'));
app.use(express.session());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});


app.jsを編集していきます。

// 前略

// まずはここ
// var express = require('express');
var express = require('express.io');

//中略

// express.ioの機能を使うため、以下を変更。メソッドチェーンしてる
// var app = express();
var app = express().http().io();

// 中略

// ここでハマった。express.ioの公式の言う通りにすること。
// それぞれのミドルウェアについて全然調べていないが、とりあえず…
// app.use(express.cookieParser('your secret here'));
// app.use(express.session());
app.use(express.cookieParser());
app.use(express.session({ secret: 'your secret here' })); // 単に文字列のままだとエラーになった

// 中略

// http.createServerする必要ない。app.http()に含まれてるのかな。
// 良く分からない。Expressのドキュメント色々信用できない。そもそも作成されるひな形が悪いのかも。

// http.createServer(app).listen(app.get('port'), function(){
app.listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

// app.io.routeでsocket.ioのイベントをハンドルする


# 実行
$ node app.js

socket.ioをセットアップすると、クライアントからは
http://localhost:3000/socket.io/socket.io.js
にアクセスすることができるようになる。これが404の場合、なんか設定間違ってる。(最後のhttp.createServer(app)の記述のままだと、404になった。)
あと、session関係の記述もミスると、TypeError: secret required とか出て落ちる。

結論

expressのひな形から作り始めるのはおすすめしない。
自分で調べてミドルウェア追加していったほうがハマりが少なそう・・・

Connection: close