幽霊型を使ってウェブアプリで安全に文字列を使う [OCaml]
以前 Joel on Software の「間違ったコードは間違って見えるようにする」という記事 [1] を読んだ。彼はこの記事の中でハンガリアン記法のうち変数のデータ型/タイプ (type) を示すに過ぎない「システムハンガリアン」を否定する一方で、タイプでは区別できないような種類 (kind) を示す「アプリケーションハンガリアン」を支持している。
この記事にはとても感心したのだけど、彼の出している「安全な文字列」と「安全でない文字列」の例をみてこうも思った。このくらいのことなら面倒見てくれるコンパイラやツールがあったっていいんじゃないだろうか。種類とタイプの違いというのは先験的にあるもの(と Joel Spolsky が書いているわけではないが)じゃなくて、コンパイラや IDE などのツールがサポートするところまでがタイプで、残りが種類なんじゃないだろうか。
とはいえ、口で言えても実際にそういうチェックを実装するのは大変そうだなと思っていたのだけど、最近購入した「入門 OCaml」にまさにこの問題へのソリューションが提示されていた。幽霊型という手法らしい。
本で例に挙げているのは「税抜き金額」と「税込み金額」の型を区別して「税込み金額に課税関数を適用できない」とか「税抜き金額と税込み金額を足し合わせることはできない」という制約をつけるものだけど、解説の終わりで「例えばWebインターフェイスから送られてきたデータをサーバ側でセキュリティチェックする際などに、[`Dirty] string と [`Clean] stringに区別できれば、不安な要素が1つ減らすことができます。」とある。これだ!というわけでざっくり作ってみた。
(* webString.mli *)
type 'a t constraint 'a = [< `Plain | `Html | `Sql | `Url ]
val make : string -> [`Plain] t
val make_html : string -> [`Html] t
val make_sql : string -> [`Sql] t
val make_url : string -> [`Url] t
val to_html : [`Plain] t -> [`Html] t
val to_sql : [`Plain] t -> [`Sql] t
val url_of_plain : [`Plain] t -> [`Url] t
val plain_of_url : [`Url] t -> [`Plain] t
val print : Format.formatter -> [`Plain] t -> unit
val print_html : Format.formatter -> [`Html] t -> unit
val print_sql : Format.formatter -> [`Sql] t -> unit
val print_url : Format.formatter -> [`Url] t -> unit
val (^) : 'a t -> 'a t -> 'a t
(* webString.ml *)
open ExtString
type 'a t = string constraint 'a = [< `Plain | `Html | `Sql | `Url ]
let make s = s
let make_html s = s
let make_sql s = s
let make_url s = s
let to_html s =
let escape = function
| '<' -> "<"
| '>' -> ">"
| '&' -> "&"
| c -> String.make 1 c
in
String.replace_chars escape s
let to_sql s =
let escape = function
| '\'' -> "''"
| c -> String.make 1 c
in
String.replace_chars escape s
let plain_of_url = Cgi.decode
let url_of_plain = Cgi.encode
let print p s = Format.print_string ("\"" ^ (String.escaped s) ^ "\"")
let print_html p s = Format.print_string ("\"" ^ (String.escaped s) ^ "\"")
let print_sql p s = Format.print_string ("\"" ^ (String.escaped s) ^ "\"")
let print_url p s = Format.print_string ("\"" ^ (String.escaped s) ^ "\"")
let (^) x y = x ^ y
ウェブアプリケーションで使用することを考えると Dirty と Clean の2種類よりはプレーン文字列、HTML文字列、SQL文字列、URLエンコード文字列の4種類くらいはあったほうがよさそうなのでそうした。コード中の Cgi モジュールは [2] で手に入るものを使っている。
これを使ってみた結果は以下のとおり。
KURO-BOX% ocaml Objective Caml version 3.09.2 # #load "str.cma";; # #load "cgi.cmo";; # #load "extlib/extLib.cma";; # #load "webString.cmo";; # open WebString;; # #install_printer print;; # #install_printer print_html;; # #install_printer print_url;; # let input = make_url "%3Cscript%3E%3C%2Fscript%3E";; (* 1 *) val input : [ `Url ] WebString.t = "%3Cscript%3E%3C%2Fscript%3E" # let decoded = plain_of_url input;; val decoded : [ `Plain ] WebString.t = "<script></script>" # let output = (make_html "<html>") ^ decoded ^ (make_html "</html>");; (* 2 *) This expression has type [ `Html ] WebString.t but is here used with type [ `Plain ] WebString.t These two variant types have no intersection # let output = (make_html "<html>") ^ (to_html decoded) ^ (make_html "</html>");; (* 3 *) val output : [ `Html ] WebString.t = "<html><script></script></html>"
(* 1 ユーザからの入力は最初 [ `Url ] WebString.t 型で与えられるものとする *)
(* 2 [ `Plain ] WebString.t 型は [ `Html ] WebString.t 型と一緒に使うことはできない *)
(* 3 to_html 関数を使って [ `Html ] WebString.t に変換すると安全に [ `Html ] WebString.t 型と連結できる *)
なかなかいい感じ。OCaml は奥が深いなあ。あとは文字コード変換をどうからめるかが課題か。
[1] http://local.joelonsoftware.com/mediawiki/index.php/%E9%96%93%E9%81%95%E3%81%A3%E3%81%9F%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AF%E9%96%93%E9%81%95%E3%81%A3%E3%81%A6%E8%A6%8B%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B
[2] http://www.lri.fr/~filliatr/ftp/ocaml/cgi/
コメント 0