Haxe
Neko
HaxeDay 7

Haxe黒魔術使い入門とWebのセキュリティの話

More than 5 years have passed since last update.

Haxeのマクロ

Haxeには コンパイル時マクロ という機能があります。

Macros(haxe.org)

とても便利な機能ですが、深い闇を持ってるのでHaxeの 黒魔術 と呼ばれています。具体的に何ができるのか、黒魔術に挑んでいった先人の記録を見てみましょう

これらの記事を見てもらえれば、コンパイル時マクロはいわばHaxeの構文そのものを捻じ曲げられる強力なもので、安易につかえばたちまち難解なコードを生み出してしまうことはわかると思います。

ということで、Haxeの黒魔術に入門する上でとても重要なことを最初に紹介します。

「コンパイル時マクロなんてものを使用してはいけません!!」

コンパイル時マクロなんて使わないぞ!と心に決めたうえで、Haxeの黒魔術に入門していきましょう。

コンパイル時マクロを使うまえに知っておくべきHaxeの機能

まず、Haxeのコンパイル時マクロを使う前に、自分がやろうとしていることがコンパイル時マクロを使わなくてもできないか、十分に検討する必要があります。黒魔術を使う前に以下のドキュメントに今一度、目を通してみましょう。

ここで特に注目しておくべきは、Haxeのコンパイル マクロという機能です。これは、コンパイル時マクロとは異なる機能で、コンパイル時マクロと比べると白い機能なので、コンパイル前マクロで事足りるものであればこちらを使って実現するべきです。

Compiler Configuration with Macros(haxe.org)

これは指定した関数をコンパイル前に実行して、パッケージ、クラス、外部リソースファイルの追加を行ったり出来る機能です。例えば、Macros(haxe.org)に載っていたコンパイルを行った時刻をプログラムに埋め込みたいといった用途であれば、こちらの機能で事足ります。

つまり、以下のファイルを記述して

pre_macro/EmbedTools.hx
package pre_macro;
import haxe.io.Bytes;
import haxe.macro.Context;

class EmbedTools{
    static public function embedDate() {
        var now = Bytes.ofString( Date.now().toString() );
        Context.addResource( "macro/build_time", now );
    }
}

コンパイラフラグに--macroをつけて、embedDate()を実行。

--macro pre_macro.EmbedTools.embedDate()

すると、外部リソースファイルとして、コンパイル時の時刻を利用することができます。

Main
import haxe.Resource;

class Main {
    static function main() {
         //2013-12-07 xx:xx:xxみたいな文字列が表示される。
         trace( Resource.getString( "macro/build_time" ) );
    }
}

他にもコンパイル前マクロを使えば、コンパイル前に単体テストを通してすべて成功した場合のみコンパイルを行ったり、自動でクラスファイルを作成してしまったり、さまざまなことが実現できます。

コンパイラマクロを使わなくてもHaxeには多くの機能があり、コンパイル時マクロが本当に必要な場合というのはとても限られています。

Webサービスのセキュリティの話

さて、話は変わってWebのセキュリティの話です。Webサービスへの典型的な攻撃方法であるSQLインジェクションを紹介します。

SQLインジェクション

SQLはデータベースへの問い合わせに使う言語のことです。

MySQLの使い方(DBOnline)

そしてSQLインジェクションは、そのSQLを標的とした攻撃方法です。

Haxeをサーバーサイドの言語として使用してデータベースを扱う場合、SPODという標準ライブラリ(いわゆるO/Rマッパー)を使用する方法と、生のSQLを文字列で記述して扱う方法があります。(Haxe/PHPであれば、PDOとかも使える)

SPODを使用する場合はSQLインジェクションを気にする必要はあまりありませんが、生のSQLを扱う場合SQLインジェクションは常に注意しなければなりません。

例えば、以下のようなコードは、SQLインジェクションに対して脆弱性があります。

SQLTools.hx
import sys.db.Connection;
class SQLTools{
    static public function selectUser( connection:Connection, id:String, password:String ) {
        return connection.request( 
            "SELECT * FROM user WHERE id = '" 
            + id 
            + "' AND password = '" 
            + password 
            + "'" );
    }
}

ユーザーがidに tarou 、passwordに hanako_love 入力した場合では、

SELECT * FROM user WHERE id = 'tarou' AND password = 'hanako_love'

というSQLが実行されるので、このコードは一見して正しいように見えます。
しかし、ユーザーがidに tarou 、passwordに ' OR ''='

SELECT * FROM user WHERE id = 'tarou' AND password = '' OR ''=''

password = '' OR ''='' という文が出来上がることで、 tarou くんのパスワードを知らなくても、 tarou くんとしてログインが出来ます。これが SQLインジェクション です。

SQLインジェクションは、パスワードなしで他人に成り済ましてログインできるだけでなくデータベース上のデータを書き換えてしまったりもできる非常に危険な攻撃です。

HaxeでのSQLインジェクション対策

上述のコードをSQLインジェクションが出来ないものに変える方法はとても簡単です。

SQLTools.hx
import sys.db.Connection;
class SQLTools{
    static public function selectUser( connection:Connection, id:String, password:String ) {
        return connection.request( 
            "SELECT * FROM user WHERE id = " 
            + connection.quote( id ) 
            + " AND password = " 
            + connection.quote( password ) );
    }
}

ユーザーから入力された値を、connection.quote()で囲ってあげれば適切に「'」がエスケープされて、SQLインジェクションが出来ないコードになります。

つまり、ユーザーがidに tarou 、passwordに ' OR ''='と言う入力をしても、実行されるSQLは、

SELECT * FROM user WHERE id = 'tarou' AND password = '\' OR \'\'=\''

となり、passwordはあくまで ' OR ''=' として検索されます。

SQLインジェクションの対策はとても簡単です。ただ、ユーザーからの入力をconnection.quote()で囲ってやればOKです。

ただし、それをやるのは人間です。もちろん、 うっかり書き忘れてしまった ということもありえます。

SQLインジェクション対策と黒魔術

さてさて、長くなりましたがようやく本題です。「コンパイル時マクロなんてものは それが本当に必要な場合を除いて 使用してはいけません」が、セキュリティを高めるためであれば背に腹はかえられません。

ヒューマンエラーを努力や心がけで防ぐのは賢くありません。そもそも、ヒューマンエラーが入り込む余地が無いような仕組みをつくりだすべきです。

つまり、ここでは

connection.request( 'SELECT * FROM user WHERE id = $id AND password = $password' );

と記述した場合に勝手に

connection.request( "SELECT * FROM user WHERE id = " + connection.quote(id) + " AND password = " + connection.quote(password) );

として解釈されるような機能をコンパイル時マクロを使って実現してしまいます。

1. Format.format関数で変数展開を行い、エスケープの関数を割り込ませる。

Haxeのマクロには、Format.format関数という、Haxe標準の変数展開と同等の変数展開を簡単に実現できる機能があります。

Format.format関数によって変数が展開された構文木を解析して、文字列の定数で無い部分にエスケープ用の関数を割り込ませていきます。

FormatMacro.hx
//マクロからしか使わないクラス
#if macro

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Format;

class FormatMacro {

    //"hoge $fuga $piyo"を"hoge " + func( fuga ) + " " + func( piyo )に置き換える。
    public static inline function escape( string:ExprOf<String>, func:ExprOf<Dynamic->String> ) {

        //"hoge $fuga $piyo"を、"hoge " + fuga + " " + piyoに置き換え。
        var formatedString = Format.format( string );

        var toStr = macro toString;

        switch( formatedString.expr ) {
            case ECheckType( expr, type ):
                var result  = null;
                var loop    = true;
                while ( loop ) {
                    var add:Expr = null;
                    switch( expr.expr ) {

                        //定数の文字列はエスケープしない
                        case EBinop( OpAdd, e1, e2 = { expr:EConst(CString(str)) } ) :
                            add = e2;
                            expr = e1;

                        case EConst(CString(_)):
                            add = expr;
                            loop = false;

                        //その他はエスケープ
                        case EBinop( OpAdd, e1, e2 ):
                            switch( Context.typeof( e2 ) ) {
                                case TInst( String, [] ) :
                                default:
                                    e2 = macro "" + $e2;
                            }
                            add = macro $func( $e2 );
                            expr = e1;

                        case _:
                            switch( Context.typeof( expr ) ) {
                                case TInst( String, [] ) :
                                default:
                                    expr = macro "" + $expr;
                            }
                            add = macro $func( "" + $expr );
                            loop = false;
                    }

                    if ( result == null ) {
                        result = add;
                    }else {
                        result = macro $add + $result;
                    }
                }
                return result;

            default: //ここには来ない。
                Context.error( "internal error", string.pos );
                return macro null;
        }
    }
}

#end

マクロで、文字列を扱う上で気を付けなければいけないのは、'$id'という文字列と、"$id"という文字列を区別出来ないという点です。Haxe 3ではシングルクオテーションと、ダブルクオテーションは別々の意味を持ちますが、この二つのどちらもが CString($id) として扱われてしまうため注意が必要です。

2. SQLのConnectionをラップするクラスを作る。

MySQLSafeConnection
#if macro

import haxe.macro.Context;
import haxe.macro.Expr;

//You cannot use the library '...' inside a macro とか言われるライブラリは、マクロ実行時に読み込まれないようにする。
#else

import sys.db.Connection;
import sys.db.Mysql;

#end

class MysqlSafeConnection {

//マクロ実行時に使わないので隠しとく。
#if !macro
    @:noCompletion
    public var connection:Connection;

    public function new ( params ) {
        connection = Mysql.connect( params );
    }

#end    

    //safeConnection.request( "hoge" )を呼び出した場合、
    //_thisには、{ expr => EConst(CIdent(safeConnetion)), pos=>... }
    //queryStringには、{ expr => EConst(CString(hoge), pos=>... }
    //が入る。
    //呼び出される側の引数は2つだけど、呼び出す側の引数1つになることに注意。
    macro public function request( _this:ExprOf<MysqlSafeConnection>, queryString ) {
        var quote   = macro $_this.connection.quote;
        var str     = FormatMacro.escape( queryString, quote );
        var result  = macro $_this.connection.request( $str );
        return result;
    }
}

ここでは、FormatMacro.escapeに対してquoteの関数を渡していますが、XSS(クロスサイトスクリプティング)攻撃に対しての対策を行いたい場合、quoteの代わりにStringTools.htmlEscapeを渡してやれば同じような変数展開を使ったXSS対策が実現できます。

3. 作成したクラスをつかってSQLを生成してみる

Main.hx
import neko.Lib;
import sys.db.Mysql;

class Main {
    static var connection;

    static function main() {
        connection = new MysqlSafeConnection( { 
            host : "localhost",
            user : "macroTestUser",
            pass : "password",
            database : "macrotest"
        } );

        selectUser( "hoge", "' OR ''='" );
    }

    static function selectUser( id:String, password:String ) {
        return connection.request( 'SELECT * FROM user WHERE id = $id AND password = $password' );
    }
}

さて実際にこのコードを実行した時のSQLの履歴をのぞいてみると…

SELECT * FROM user WHERE id = 'hoge' AND password = '\' OR \'\'=\''

ちゃんと、エスケープされていることが分かります。

そしてこのようにマクロを使ってエスケープを行うことで、脆弱性を持つコードがあるとそもそもコンパイルできなくなります。

つまり、以下のようなコードがコンパイルエラーになります。

//Constant string required
connection.request( "SELECT * FROM user WHERE id = '" + id + "' AND password = '" + password + "'" );
var query = "SELECT * FROM user WHERE id = '" + id + "' AND password = '" + password + "'";

//Constant string required
connection.request( query );

こうやって人為的なミスが起こる余地をなくしてやることで、Webサービスのセキュリティはぐっと高まるでしょう。

最後に

黒魔術ことコンパイル時マクロが本当に必要になる場面を考えてみましたが、自分が思いついたのはセキュリティ対策くらいでした。ちなみに、先述のSPODもコンパイル時マクロを利用するものですね。こちらも、セキュリティ的な理由があってコンパイル時マクロを使用しているのだと思います。

Haxeにはコンパイル時マクロ以外にも、typedef条件付きコンパイルusing型パラメータabstract型コンパイル前マクロ外部リソースの埋め込みなど強力な機能がたくさんあります。そして、必要な機能の多くはこれらを使えば実現できます。黒魔術に手を出す前に別の方法を十分に検討しましょう。黒魔術は、用法、用量を守って正しく使いましょう。

以上、Haxe黒魔術使い入門でした。

明日は、@fukaoi@githubさんの予定です。よろしくお願いします。