wasbook reading #3

「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」読書会の第3回。
今回読んだのは以下の範囲。今回は軽め。

  • 4章 Webアプリケーションの機能別に見るセキュリティバグ

4章 Webアプリケーションの機能別に見るセキュリティバグ

SQL呼び出しに伴う脆弱性

最近だと NoSQL Injection もあるらしい。MongoDBについて書いてあった。

Consider this query:


db.foo.find( { $or : [ { a : 1 } , { b : 2 } ] } )
If the number 2 in the query above was coming from concatenation of string input, we might get the following injection:

db.foo.find( { $or : [ { a : 1 } , { b : 2 }, { c : /.*/ } ] } )
NOSQL-injection

なるほど。HTML5のIndexedDBとか、localStorageも同じようなことできるかも。悪用するのは、ちょっと難しいかもしれないけど。


直接のSQLインジェクション対策ではないけど、DBを管理者権限で動かすといろいろ大変だよね。saとか。あと、SQLインジェクションが攻撃の契機だけど、実際にはコマンド実行されて、ごにょごにょされちゃうことも多い気がする。
XSSは人間が職人芸で発見していることが多い印象だけど(たぶん人が見つけたものが話題になっていて、ツールでの攻撃も多いと思う)、SQLインジェクションは主にツールで攻撃されているような気がする。ツールもDB系の詳細なエラーメッセージとかを解釈して、攻撃しているのだろうか。あるいはある程度人が操作しているのかな。うーん、わからない。


文字列リテラルのところで「O'Reilly」がサンプルになっていた。シングルクォートを自然に含んでいる。ところで入力boxに「O'Reilly」を入力して、詳細なエラーメッセージに「構文が不正です」とか「リテラルが閉じられていません」とかがでてきてしまったら、どうすればいいのだろう。IPAの報告フォームを見たことがないのだけど、これだけでも受け付けてくれるものだろうか。SQLインジェクションなリクエストを送ってしまうと、それは非常にまずいと思うので。


SQLインジェクション対策として、プレースホルダを使うのはもはや常識だと思う。Javaだと PreparedStatement になる。基本的に PreparedStatement はあまりデメリットがないけど、ひとつ非常に困ることがある。それはSQLのログ出力だ。PreparedStatement を利用するとSQLはこんな感じになる。

String sql = "select tname from tab where tname = ?";
PreparedStatement pstmt = con.prepareStatement(sql);

このあと ? に変数をバインドして、execute して SQL を発行する。
それで、実際に実行した SQL のログが欲しい。バインドされた状態のSQLが知りたい。

select tname from tab where tname = "emp"

みたいな感じのログが欲しい。でもこれがなかなか難しい。
O/R Mapper を使っていれば、フレームワークが出してくれたりするけど、普通には出てこない。Oracleだとデバッグ用のJDBCドライバとか使わないとだめそう。DBで SQL Trace 取るのは面倒だし。結局調べてみても、あまりよい方法はなさそうだった。
S2Daoのソースを読んでみると、ログ出力用に SQL の ? に変数を埋め込んでいる。(これはログ出力用で、PreparedStatementで実行されている)
org.seasar.dao.handler.BasicUpdateHandler をちょっとだけ読んだ。

    protected String getCompleteSql(String sql, Object[] args) {
        if (args == null || args.length == 0) {
            return sql;
        }
        StringBuffer buf = new StringBuffer(200);
        int pos = 0;
        int pos2 = 0;
        int index = 0;
        while (true) {
            pos = sql.indexOf('?', pos2);
            if (pos > 0) {
                buf.append(sql.substring(pos2, pos));
                buf.append(getBindVariableText(args[index++]));
                pos2 = pos + 1;
            } else {
                buf.append(sql.substring(pos2));
                break;
            }
        }
        return buf.toString();
    }

    protected int execute(CommandContext ctx) throws SQLRuntimeException {
        Connection connection = getConnection();
        try {
            sqlWrapper.preUpdateBean(ctx);
            rootNode.accept(ctx);
            String sql = sqlWrapper.transformSql(ctx.getSql());
            Object[] args = ctx.getBindVariables();
            Class[] argTypes = ctx.getBindVariableTypes();
            if (logger.isDebugEnabled()) {
                logger.debug(getCompleteSql(sql, args));
            }
            int ret = execute(connection, sql, args, argTypes);
            sqlWrapper.postUpdateBean(ctx, new Integer(ret));
            return ret;
        } finally {
            ConnectionUtil.close(connection);
        }
    }

execute(CommandContext ctx) のなかの logger.debug(getCompleteSql(sql, args)); でログを出力している。getCompleteSql(sql, args) でSQLと変数を渡して StringBuffer でがりごり連結している。実際に実行されるのは PreparedStatement になる。Utilとかが入っているので最後まで追ってないけど。自分で getCompleteSql 相当のものを実装して、execute の前に実行するようにしたユーティリティとか作ればいいけど、それはすなわち オレオレDaoフレームワーク になってしまう気がする。


NodePreparedSqlBuilder.java
Domaだと、本体のSQLを組み立てるときに同時にログ用の文字列も組み立ているとのこと。こっちもちゃんと追えていないけど。
やっぱりそれなりに工夫しないとだめみたい。あと機密情報などはログにださないように気をつける必要がある。


プレースホルダが使えない場合の対策で、エスケープなどが記載されていた。これは当然のことだけど、プレースホルダへの変更が出来ないケースで、エスケープの対策を組み込めると思えない。本質的な対応ではないけど、現実的にはWAF(Web Application Firewall)の導入が効果的だと思う。


静的プレースホルダと、動的プレースホルダについては、本を読んで初めて知った。デフォルトだと動的プレースのほうが多そうで、静的プレースホルダだと性能について問題がないのか気になった。

今回のまとめ

SQLインジェクションは、XSSに比べると対策がはっきりしていると思う。
第3回で140ページくらいまでたどり着いた。予習も大変だけど、こうやってblogに書く復讐も大変。それでも、これで約1/3は読み終わった。ひとりだったら絶対に挫折していた。このペースなら全10回で終われそう。


wasbookに関するものは、こちらのタグで。



電子書籍版はこちら。