Haxeを使うことのメリットの1つは、サーバーとクライアントを同じ言語で開発できることです。
ただし、この場合サーバーとクライアント間の通信をどのようなフォーマットで行うかは悩ましい問題です。
通信につかうフォーマットとして、まず思いつくのがhaxe.Serializerのフォーマットです。シリアル化を使えば、別々のプログラム間でHaxeのオブジェクトを共有することが簡単にできます。サーバーとクライアント間の通信で使うには最適なようにも見えます。
ですが、実際のところhaxe.Serializerを通信で使うとセキュリティ上の問題を抱えることになります。
クライアントサイドから受信したデータを、Unserializer.runして使ってはいけない
以下に、サーバーサイドのHaxe/Node.jsのサンプルコードを書きました。このコードはセキュリティ上の問題を抱えています。
import haxe.Unserializer;
import haxe.Http;
import js.Node;
class Server {
static function main() {
// httpサーバーを立てる。
var server = Node.http.createServer(handleRequest);
server.listen(5004);
}
static function handleRequest(request:NodeHttpServerReq, response:NodeHttpServerResp) {
try {
//リクエストのヘッダとURLを表示。
trace(request.headers);
trace(request.url + "\n");
//urlクエリから文字列を取得
var url = Node.url.parse(request.url);
var data = StringTools.urlDecode(url.query);
//取得した文字列を逆シリアル化。
var user:User = Unserializer.run(data);
user.request(); // ユーザーからのリクエストを処理。
//レスポンスをかえす。
response.end();
} catch (d:Dynamic) {
trace(d);
}
}
}
class User {
public var id:String;
//ユーザーごとのリクエスト数をカウントする関数。
public function request () {
trace("ユーザー" + id + "がアクセスをしました");
// ここにリクエスト数をカウントする処理が入る。
}
}
このコードでは、クライアントサイドからシリアライズ化されたUserクラスのインスタンスを送ってくることを想定しており、そのユーザーごとのアクセス回数を数えています。
問題となる箇所は、逆シリアル化で得られたインスタンスのrequest関数を呼び出しているところです。 この関数は本当にUserクラスのrequest関数なのでしょうか? もしかすると本当は、haxe.Httpクラスのrequest関数なのかもしれません。
例えば、クライアントサイドJSから以下のようコードで、アクセスされた場合を見てみます。
import haxe.Http;
import haxe.Serializer;
class Client
{
static function main() {
var nextHttp = new Http("http://localhost:5004/?hellohello");
// Node.js用での逆シリアル化用に型をあわせる。
untyped nextHttp.headers = new Map<String,String>();
untyped nextHttp.params = new Map<String,String>();
var http = new Http("http://localhost:5004/?" + StringTools.urlEncode(Serializer.run(nextHttp)));
http.request();
}
}
このコードでは、Userクラスのインスタンスではなく、Httpクラスのインスタンスをシリアル化してサーバーに送っています。
このコードを実行したときのサーバーサイドの出力を見ています。
{ host: 'localhost:5004',
connection: 'keep-alive',
'user-agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/39.0.2171.71 Safari/537.36',
origin: 'http://localhost:2000',
accept: '*/*',
referer: 'http://localhost:2000/bin/index.html',
'accept-encoding': 'gzip, deflate, sdch',
'accept-language': 'en-US,en;q=0.8,ja;q=0.6' }
/?cy9%3Ahaxe.Httpy3%3Aurly45%3Ahttp%253A%252F%252Flocalhost%253A5004%252F%253Fhe
llohelloy7%3Aheadersbhy6%3Aparamsbhy5%3Aasynctg
{ host: 'localhost:5004', connection: 'close' }
/?hellohello
ブラウザからのリクエストが1件、そして自分自身からのリクエスト1件あります。HttpクラスはUserクラスと同じくrequestという名前の関数をもつのでこれが呼び出されてしまいました。
つまり、このコードには任意のHttpリクエストを送れる脆弱性があるということです。
これにより、IP制限がされているAPIにアクセスしたり、他サーバーへの攻撃に利用することが可能になるわけです。
そして重要なのは、これがオブジェクトをシリアル化して通信することの問題の一部でしかないことです。たとえば、関数名がデータベースを操作する関数とかぶったら、外からデータベースの操作が可能になります。関数名が OSコマンドを実行できる関数とかぶったら、任意コマンド実行の脆弱性 になりえます。
この危険性はプロジェクトの規模が大きくなるほどに増して、大きいプロジェクトではこの危険性が無いことの検証は非常に難しくなります。
安全性のことを考えれば、そもそもとしてクライアントサイドからの受け取とったデータのUnserializer.runを使って逆シリアル化してはいけません。
Haxe Remoting
Haxeは標準ライブラリで、Haxe Remotingという機能を提供しています。これはクライアントから簡単な関数呼び出しで、サーバーサイドの関数にオブジェクトを渡せる仕組みでとても便利そうに見えます。ですが、これも内部的にはSerializerを使用しています。つまり、これも全く同様の危険性を持っています。
じゃあ、何を使うべきなのか?
基本的には、UnserializerにはsetResolverという関数があり、これを使って逆シリアライズによって生成されうる型を限定してあげれば、これを解決することができます
ほかに、無難な方法はhaxe.format.JsonPrinterとhaxe.format.JsonParserを使うことですが、これらを使うとenumなどが送信することができません。
また、Haxe Serializerとは異なるシリアライザを用意すれば、サーバー/クライアント間で、安全にenumなどを含むデータの共有を行うことができます。
例えば、マクロを使って、型付けができるJSONシリアライザを自前で実装するとか。ということで、匿名構造体の型を書くことで型付けができるシリアライザを実装した話を書きたいのですが、長くなってしまうので、この続きは来週あたりにまた書きます。
といことで、今回はここまで。
明日のHaxe Advent Calendarの担当は@mandel59さんです。