Hiccup を使うときはエスケープ問題にご注意


先日アドベントカレンダーで書いたといったドキュメントが思いの外好評ぽかったので安心しているところですが、一点だけ注意しないといけないことがあります。

とりあえず以下のようなコードを書いてみます。

(ns demo.core
  (:require [hiccup.core :as h]
            [immutant.web :as web]
            [ring.util.response :as res]))

(defn handler [req]
  (let [user-input "<script>alert('(\\\\( ⁰⊖⁰)/)')</script>"]
    (-> [:div user-input]
        h/html
        res/response
        (res/content-type "text/html; charset=utf-8"))))

(web/run #'handler {:port 3000})

するとあら不思議。

このようにアラートが画面に出てしまいます。これは Hiccup が自動的に文字列をエスケープしてくれないためです。ではどうしたら良いかというと次のようにします。

(defn handler [req]
  (let [user-input "<script>alert('(\\\\( ⁰⊖⁰)/)')</script>"]
    (-> [:div (h/h user-input)] ;; hiccup.core/h を使うことで文字列をエスケープすることが出来る
        h/html
        res/response
        (res/content-type "text/html; charset=utf-8"))))

はい、これで安心ですね。

ホビー/学習用途で Hiccup を使う場合、そこまで気にする必要がないかもしれませんが実プロダクトで使うときには気を付けてください。

補足

自動的にエスケープしてくれるテンプレートエンジンが多い中、 Hiccup のような存在は珍しいと思います。この問題は Issue にもあがっているのですが、 Hiccup 自身を根幹から作り変えないと対応出来ない上に対応中のブランチも開発が止まっているようでなかなか改善されないでいます。

Hiccup のような記述方法は使いたいけど、いちいちエスケープをするのが嫌だ、という方は次のように Enlive を使うことで問題を解決できます。

resources/template/main.html
<!doctype>
<html>
    <body>
    </body>
</html>

HTML のテンプレートを用意しておいて…

(ns demo.core
  (:require [immutant.web :as web]
            [net.cgrand.enlive-html :as html]
            [ring.util.response :as res]))

(html/deftemplate layout "template/main.html" [content]
  [:body] (html/content content))

(defn handler [req]
  (let [user-input "<script>alert('(\\\\( ⁰⊖⁰)/)')</script>"]
    (-> [:div user-input]
        html/html
        layout
        res/response
        (res/content-type "text/html; charset=utf-8"))))

こうすれば、 Hiccup の記述力を失わずにセキュアなコードを書くことができます。ベター Hiccup としても使える Enlive は優秀です。

(ドキュメントの方は後で注意書きを足しておきます)