Clojure Web Development勉強 - Hiccup (その2) XSSの恐怖
なにやら五島勉のようなタイトルをつけて恐縮だが、Hiccupでも良く指摘される重大な問題として、自動的に文字列をエスケープしないことによるXSS脆弱性の導入がある。
つまり、今まで紹介してきたコードにもあるように、「フォームなどでユーザインプットを受け取りそれをHTMLに埋め込んで返す」というような挙動の場合、そのインプットに任意のJavaScriptコードなどが入っていると、それをユーザのブラウザ上で自動的に走らせてしまう恐れがある。恐ろしいのは「あるユーザの入力したスクリプトが別のユーザのブラウザで走る場合」そして「そのスクリプトが与えられている権限が、サイト全体の権限と同一で、一般的な権限よりも大きい場合」である。
それを避けるために「文字列をエスケープする」、つまり「タグやコードとして認識されるような部分を無理やり出力するべき文字として認識するようエスケープ記号などを挿入する」ことが重要になる。
そういえばこの話題はJoel on Softwareでも言及されてたな。
Let’s put it in pseudocode. Imagine that
s = Request("name")
reads input (a POST argument) from the HTML form. If you ever write this code:
Write "Hello, " & Request("name")
your site is already vulnerable to XSS attacks. That’s all it takes.
ということで、今までのHTMLベタ打ちなコードではこの脆弱性が存在していた。
前述の通りHiccupは自動的には文字列をエスケープしてくれない。
(html [:div "<script>alert()</script>"])
のようなコードは
"<div><script>alert()<script></div>"
と変換されてしまう。
ただしくエスケープするためには、Hiccupが提供しているh関数で文字列を変換してやる必要がある。例えば
(html [:div (h "<script>alert()</script>")])
だと結果は
"<div><script>alert()</script></div>"
で、ブラウザでもちゃんと文字列として表示されるようになる。
前回のコードで動的にユーザインプットを表示していたのはviews.cljのdisplay-result関数である。GET/POSTフォームのパラメータからnameを取り出してHTMLに埋め込んでいた。
(defn display-result [req] (let [{:keys [params uri]} req param-name (get params "name") req-type (if (= uri "/get-submit") "GET" "POST")] (html [:div [:h1 "Hello " (h param-name) "!"] [:p "Submitted via a " req-type " request."] [:p [:a {:href ".."} "Return to main page"]]])))
ちゃんと(h param-name)
となっているので、例えば入力された値が</h1><script>"Hello XSS!"</script><h1>
などというJavaScriptが含まれたものでも、ちゃんとウェブページ上で
Hello </h1><script>alert("Hello XSS!")</script><h1>!
と表示される。
ちなみに(h param-name)
をparam-name
にして再実行すると、Chromeの場合JavaScriptは実行されず、Console Logでこんなメッセージが出る:
The XSS Auditor refused to execute a script in 'http://localhost:8080/post-submit' because its source code was found within the request. The auditor was enabled as the server sent neither an 'X-XSS-Protection' nor 'Content-Security-Policy' header.
モダンブラウザは偉大である。が、とりあえずモダンじゃないブラウザもまだ稼働してそうなことだし、やはりXSS対策は開発者側のほうでできる限り対応するべきだろう。ということでh関数はユーザインプットを受け取るところではいたるところで使うべきだろう。
最後に参考文献的なものを。
Clojure Web Security全般についてはこの発表が面白かった。
このどちらかというと問題山積みな状況から二年半でClojureのセキュリティ関連のエコシステムがどう変わったのか、とても興味がある。Web開発の勉強を進めていく上で気をつけたいポイントである。
XSSそのものではないが、似たようなセキュリティ問題について:
セキュリティは大きな問題なので体系的かつ早期に学ぶことが望ましい(多分)。というわけでずっと積ん読していたこれをぼちぼち読んでいる。
いろんな脆弱性があるなぁ・・・(遠い目 ウェブ開発は大変である。
追記: このエントリ書いていて知ったんだけど、Hatena BlogってユーザインプットからJavaScript実行するのな・・・ Preview機能を使ったら突然Alertポップアップが開いて驚いた。(修正済み)
Clojure Web Development勉強 - Hiccup (その1)
今まで愚直にHTMLタグを文字列として書いてきたが、読むほうも辛かったと思うが書くほうはもっと辛かった。
まあ、ただこの時点では大して動的な機能もなく(唯一あったのはフォームから受け取った文字列を表示するくらい(ただしこれが「くらい」というには問題のありすぎることなのは後述する))、複雑なテンプレート機能を使う必要があったわけじゃないのも事実である。
ここから「すべてのページで共通の部分を表示させたい」「同じようなエレメントを複数回表示させたい」などといったニーズが生じていくにつれ、さらに「HTMLタグを文字列で」というアプローチは辛くなっていく。
ということで、より動的にHTMLを作成するテンプレートDSLを提供するライブラリHiccupを使ってみる。
HiccupはHTMLテンプレートとしてもかなり特殊で、Clojureの標準データ構造であるVectorとMapでタグとプロパティを表現する。
例えば
<div> <h1>Hello</h1> <p>My HTML document</p> </div>
はHiccupだと
[:div [:h1 "Hello"] [:p "My HTML document"]]
となる。
いちいち</h1>
と閉じなくて良くなった、というのはまあ小さなメリットである。より重要なメリットとして、Clojureデータ構造として表現されている以上、Clojureの豊富なデータ変換関数を使って非常に高い自由度で以って変換したり組み合わせたりすることが可能となっている。
Hiccupを使って今までのコードを書き直してみた。
基本的に変更はview.cljの部分。hiccup.core
からhtmlマクロとh関数を使っている。
htmlはClojureデータ構造で定義されたhtml構造を正しいHTML文字列に変換するhiccupの根本となるマクロ。
REPLで試してみるとこんな感じ:
user=> (use 'hiccup.core) nil user=> (html [:div #_=> [:h1 "Hello"] #_=> [:p "My HTML document"]]) "<div><h1>Hello</h1><p>My HTML document</p></div>"
インデントは(当然といえば当然)再現されない。
なのでview.cljに定義されている関数は今までどおりHTMLタグ入りの文字列を返している。関数内部の表記がhiccupなだけである。
user=> (ns simple-hiccup.views) nil simple-hiccup.views=> (get-form {}) "<div><h1>Hello GET Form!</h1><p>Submit a message with GET</p><form action=\"get-submit\" method=\"get\"><input name=\"name\" type=\"text\" /><input type=\"submit\" value=\"submit\" /></form><p><a href=\"..\">Return to main page</a></p></div>"
個人的には、ここまで使った範疇だけでもHTMLの記法としてかなり良くできていて、HTMLタグを打つより断然Hiccupで書きたいと感じているのだが、これは個人の好みの問題かもしれない。近いうちにhiccupのより動的な機能や再利用性の向上を例を挙げて書きたい。
しかし、次の話題はhiccup.core
から呼び出しているもう一つの名称であるh関数とXSS攻撃についてである。
Clojure Web Development勉強 - Compojure (その2)
ちょっと前回の説明で書いていなかったことを補足。
Compojureで使ったdefroutes
マクロとGET
やnot-found
といった個別のルート作成マクロ・関数の引数と戻り値について。
まず、GETやnot-foundは普通に関数を返す。返された関数のシグネチャは当然「リクエストマップを引数に受け取り、レスポンスマップを戻り値に返す」というハンドラの標準のものだ。
(def main-handler (GET "/" [] "Hello Main"))
というようにマクロを実行し、関数を作成して特定の名前に束縛することも当然できる。
(main-handler request)
と関数を実行すると、引数のrequestがマップでrequest-methodがgetでuriが"/"であれば
{:status 200, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Hello"}
こんな感じのマップが返ってくる。
ちなみに先ほど「引数のrequestがマップでrequest-methodがgetでuriが"/"であれば」と書いた。実際、引数がマップでこの二つのフィールドが存在しその値が正しければ、他のフィールドが何であれ(むしろ他のフィールドは存在しなくても)GET
で作った関数は普通に適用される。REPL上でやってみると
=> (main-handler {:request-method :get :uri "/"}) {:status 200, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Hello"}
といった按配である。
not-found
の返す関数はさらにゆるくて、どんな引数でも
=> ((not-found "Not Found") {:uri "/random-stuff" :request-method :get}) {:status 404, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Not Found"} => ((not-found "Not Found") {}) {:status 404, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Not Found"} => ((not-found "Not Found") nil) {:status 404, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Not Found"} => ((not-found "Not Found") "a") {:status 404, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Not Found"}
のように、同じ404エラーを返す。引数がマップである必要すらない。
ではdefroutes
の方はどうだろう。defroutes
の定義に使われる関数は基本的にリクエストマップを受け取ってレスポンスマップを返すわけだが、戻り値に関してdefroutes
の方で何らかの制限を設けているだろうか?
こういうルーティングをdefroutes
で定義してみる:
(defroutes routes (GET "/" [] "Main") (fn [req] (if (= (:uri req) "/test1") {:a "b"})) (fn [req] (if (= (:uri req) "/test2") {})) (fn [req] (if (= (:uri req) "/test3") "b")) (fn [req] (if (= (:uri req) "/test4") nil)) (not-found "Not Found"))
それで:uriを"/test1"から"/test4"まで投げてみると、戻り値はnil
でなければ何でもいいことがわかる。nil
の場合はマッチしなかったと看做されてさら次の関数(この場合はnot-found)から順次適用されることになる。
とまあGET
、not-found
、defroutes
と全てがかなり薄く単純なデータと関数をベースにラップしている機能だということがわかった。例えばバリデーションなどはやってないに等しい。そしてそれは例えばdefroutes
を使っているからといってGET
を使う必要は必ずしもなく、逆もまた然り、ということになる。
こういう簡単な関数を、推奨される方法はあるにしろある程度自由に組み合わせていってアプリを作成する、という文化はなかなか好きかもしれない。
Clojure Web Development勉強 - Compojure (その1)
さて、Ringに続いてClojure Web Developmentでメジャーなライブラリ/フレームワークであるCompojureを使ってみる。
ちなみにここらへんも含めてCompojureに関しては@_ayato_pさんにいろいろご教授いただいた。あとウェブ開発の資料も大変参考にさせていただいている。
@zehnpaard 出来ます(笑)
— お金稼ぐのに飽きた (@_ayato_p) 2016年10月25日
@zehnpaard この話はこれで一度書いたんですよ。https://t.co/V8mcFDKFGN
— お金稼ぐのに飽きた (@_ayato_p) 2016年10月25日
多謝。
Compojureはたまにフレームワークとして紹介されているが、現時点ではただのライブラリだと思う。とにかく責務がかなり限られている。が、限られているようで微妙に複数の異なることをやっていて、それが個人的にはちょっと鼻につく。
githubのドキュメントの最小例を見てみるとこんな感じになっている:
(ns hello-world.core (:require [compojure.core :refer :all] [compojure.route :as route])) (defroutes app (GET "/" [] "<h1>Hello World</h1>") (route/not-found "<h1>Page not found</h1>"))
こんな小さな例なんだから:refer :all
じゃなくて[defroutes GET]
してほしいとか思ったり思わなかったり。とにかくこのGET
やらroute/not-found
やらで「requestがこんなパターンにマッチしたらこういうresponseを返す」というロジックを作り、それらをまるでcond
のようにつなげているのがdefroutes
。ちなみにdefroutes
とGET
はマクロ、route/not-found
は関数。
今まで書いてきたようにringでこれを実現するとしたら:
(ns hello-world.core (:require [ring.util.response :as res])) (defn app [req] (cond (= (:uri req) "/") (-> (res/response "<h1>Hello World</h1>") (res/content-type "text/html")) :else (-> (res/not-found "<h1>Page not found</h1>") (res/content-type "text/html"))))
という形になるので、確かにCompojureを使うと綺麗になっていて書きやすい。
気をつけないといけないのは、GETが同時に二つのことをしている点で、
- uri(以外にもparameterなどでもできる)でパターンマッチングをしている
- ring.util.responseのように文字列を装飾してレスポンスマップの形式に変換している
と、一粒で二度美味しいとみるか、責務が違うものが同じマクロで同時に展開されていてちょっと分かり難いとみるか。
個人的には、ここにさらにuriとparameterのdestructuringでローカル変数束縛の機能が入ると、ちょっと異なる責務が同じマクロに集中しすぎている印象を持ってしまう。
それを踏まえて、前回のコードをcompojureを使った仕様で書いてみた:
前回handler.clj
と呼んでいたファイルをroutes.clj
とviews.clj
に分割。viewsにはリクエストマップを受け取り(not-foundの場合は引数なし)、レスポンスのbodyとなるテキストファイルを返す関数を集めてある。routesが今回追加のcompojureの部分で、ライブラリで定義されているdefroutes
、GET
、POST
、not-found
とviews.clj
の関数を使って最終的なringハンドラ関数app
を作っている。
これをcore.clj
でmiddlewareに包んでringのjetty adapterに渡しているのは従来の通り。
現在の悩みどころとしては、views.clj
のほぼ全ての関数をreqをとってresを返すというシグネチャに統一しているデザインでいいかどうか。特に、display-result
の引数をより分割されたものにして、ルーティングのGET
マクロが持つdestructuring機能をより有効に活用すべきかどうか。
(defn display-result [req] (let [{:keys [params uri]} req param-name (get params "name") req-type (if (= uri "/get-submit") "GET" "POST")] (str "<div> <h1>Hello " param-name "!</h1> <p>Submitted via a " req-type " request.</p> <p><a href='..'>Return to main page</p> </div>")))
見ての通り、現在だとdestructuringはこのviewの関数の中で全てやっている。個人的にはこっちの方がいい気がするのだが、いろいろ書いているうちに意見が変わるのかもしれない。
次回はHiccupというClojure特有のHTML Templateライブラリを使って、HTML文字列を直接コードに書き込まずに済むようにしたい。
Clojure Web Development勉強 - Ring (その5) ルーティング
というわけでついにルーティング。
そもそもルーティングとは?
今まではどのリンクを踏んでも同じコンテンツしか見えないという「お前htmlメモ帳に直打ちしてftpでアップロードしてた頃のサイトの方がよっぽど動的なんだけどどうなの?」と煽られてしまうような有様だったのだが、ようやく「uriやparamsによってどのようなページを表示するかをプログラム上で決定する」という機能を実装する。この機能がルーティング。
前回のプログラムにルーティングを実装したコードがこちら:
変更したのはhandler.cljとcore.cljの2ファイル。
handler.cljでの変更: - get/postからのクエリパラメータを使って非常に簡単な動的HTML作成を実施するdisplay-result関数 - リクエストマップの中のuriやparamsフィールドなどを調べ、正しいhandlerへのルーティングを担当するhandler-with-routing関数 - コードの整理(使わないplain-textやredirect関数を削ったり、微妙に表示されるhtmlを変更したり)
core.cljでは単にhandler-with-routing関数を大本のhandlerとして使うように変更したのみ。
display-result関数の中身:
(defn display-result [req] (let [{:keys [params uri]} req param-name (get params "name") req-type (if (= uri "/get-submit") "GET" "POST")] (-> (res/response (str "<div> <h1>Hello " param-name "!</h1> <p>Submitted via a " req-type " request.</p> <p><a href='..'>Return to main page</p> </div>")) (res/content-type "text/html"))))
destructuringでリクエストマップからparamsとuriをとり、さらにparamsから"name"フィールド、uriがpost-submitかget-submitかによってreq-typeを"GET"か"POST"に束縛して、名前とリクエストタイプを表示する。言ってしまえばそれだけだが、最低限の動的な機能は備えている。
そしてルーター部分:
(defn handler-with-routing [req] (println "In Routing Handler!") (let [{:keys [uri params request-method]} req] (cond (= uri "/") (main req) (= uri "/get-form.html") (get-form req) (= uri "/post-form.html") (post-form req) (= uri "/get-submit") (display-result req) (= uri "/post-submit") (display-result req) :else (not-found req))))
ちなみに私はどーしてもラウター、ラウティングと読んでしまう・・・
まあボイラープレートっぽい(= uri ...) (... req)
が並んでいる時点でLisperなら誰でも「おっ、マクロの時間だ」と思うわけで、近いうちに使ってみることになるCompojureというライブラリも基本的にこの部分を簡略化した上で強力なパターンマッチング機能を提供するマクロ群だ。
しかし、見栄えは別にして、これでこのサーバ上で何かのページにアクセスしようとした場合はこの関数を通してuriマッチングが行われて正しいページに送られるようになったわけだ。この関数に載っていないuriの場合は:elseのところにかかって(not-found req)から404メッセージが表示される。
ここからはring以外のライブラリ、特にcompojureとhiccupを見ていきたい。compojureでルーティング周りを、そしてhiccupでhtmlページの記述をより洗練させていきたい。
Clojure Web Development勉強 - Ring (その4) モジュール化とGET/POST Form
次回はルーティングだと言ったな、あれは嘘だ。
その前に、まず今まで書いたコードを
- handler.clj (responseを実際に返す関数群)
- middleware.clj (handlerをラップしてログを取ったりする機能を実装する関数群)
- core.clj (それ以外。main関数っぽいのも含めて)
に分割してみた。
そしてGET/POST formを使ってクライアントからサーバ側により自由な形式で情報を送れるようにしてみた。
そのコードがこれ:
分割したので、何かのファイルを更新するたびに(require simple-modular-ring.core :reload-all)
としてREPLに全部のdependencyをリロードさせる必要が生じている。vim fireplaceだとcore.cljのファイルから:Require!
でやってくれるので便利。
コードの説明をすると、まずmiddlewareで
(ns simple-modular-ring.middleware (require [ring.middleware.params :as mparams])) (def wrap-params mparams/wrap-params)
ringが提供する便利middlewareの一つであるwrap-paramsを使っている。paramsとはGETやPOSTメソッドでクライアントから渡されるurl以外の情報(例えばフォーム入力)を集めたもの。wrap-paramsを使わないと、特にPOSTメソッドの場合その情報がリクエストマップから失われてしまう。ここら辺の事情についてはringの公式ドキュメントが詳しい。
そして今回は結構リクエストマップを調べるので、リクエスト・レスポンスを@log-atomに入れるだけじゃなくてreplに直接pprintしてくれるmiddlewareを作った。
(defn req-res-displayer [handler] (fn [req] (let [res (handler req)] (println "\nRequest:") (clojure.pprint/pprint req) (println "\nResponse:") (clojure.pprint/pprint res) res)))
pprintした後、ちゃんとレスポンスを返り値として書いておくのを忘れないように・・・
さて、次は今回の主役と言っていいGETとPOSTのフォームを表示するハンドラである。
まずはGET:
(defn get-form [req] (-> (res/response "<div> <h1>Hello GET Form!</h1> <form method=\"get\" action=\"form-submit\"> <input type=\"text\" name=\"name\" /> <input type=\"submit\" value\"submit\" /> </form> </div>") (res/content-type "text/html")))
これをappのハンドラとして設定すると、リクエストマップが大体以下のようになる:
{;いろいろ略 :protocol "HTTP/1.1", :params {}, :headers {;以下略 }, :server-port 8080, :form-params {}, :query-params {}, :uri "/", :server-name "localhost", :query-string nil, :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x89db6db "HttpInputOverHTTP@89db6db"], :scheme :http, :request-method :get}
で、フォームに適当な文字列を入れてSubmitを押すと、アドレスバーのURLが"localhost:8080/form-submit?name=Test%21"となる。REPLを調べてみると新しくこのようなリクエストマップが投げられているのがわかる:
{;さらにいろいろ略 :params {"name" "Test!"}, :form-params {}, :query-params {"name" "Test!"}, :uri "/form-submit", :query-string "name=Test%21", :request-method :get}
params、query-params、query-string全部に"Test!"という文字列が含まれている(query-stringの場合アドレスバーに!が表示できないので%21になっているが)。この情報を受け取って、ハンドラはよりカスタムなページを作成・表示させることができる。例えばその文字列をHelloの後に表示するとか。
POSTも大体流儀は同じなのだが、そもそもアドレスバー経由じゃないのでquery-paramsにはならない。
(defn post-form [req] (-> (res/response "<div> <h1>Hello POST Form!</h1> <form method=\"post\" action=\"form-submit\"> <input type=\"text\" name=\"name\" /> <input type=\"submit\" value\"submit\" /> </form> </div>") (res/content-type "text/html")))
こっちのフォームに文字列を入れるとURLが"localhost:8080/form-submit"に変わり(今度はqueryの内容はURLに反映されない)、サーバの方ではこんなリクエストを投げてくる:
{;またしても省略 :params {"name" "Test!"}, :form-params {"name" "Test!"}, :query-params {}, :uri "/form-submit", :query-string nil, :request-method :post}
やはりクエリには何も含まれておらず、paramsとform-paramsに情報が入ってくる。前述の通り、これらはwrap-paramsを使わないとリクエストマップに含まれない。
GETとPOSTで大体同じ機能を実装してみたわけだが、HTMLやウェブ入門的な文書によると、基本的にリクエストに応じて情報を引っ張ってくるだけ&クエリ内容にプライバシーの心配がない場合GETを使い、副作用的な効果のあるリクエスト(掲示板への投稿など)や情報を秘匿した方がいいもの(ユーザ名やパスワード)はPOSTを使う、らしい。POSTを使うのも、今のベストプラクティスかどうかはわからないのでその部分は信用しないでいただきたい。
そして次回は今度こそ、uriやparamsを用いて動的にどのページを表示するか、ringアプリの方で決めるコード、つまりルーティング機能を(とりあえずまずはringだけで)実装していく。
Clojure Web Development勉強 - Ring (その3) ResponseとHandler
いろんなresponseの作成を助ける関数群のring.util.responseを少し使ってみる。
前回handlerと読んでいた関数をplain-textと改名し、それに加えてhtmlを返すhtml関数、404を返すnot-found関数、そしてhttp://www.google.comにリダイレクトするredirect関数を定義した。
html関数:
(defn html [req] (-> (res/response "<div> <h1>Hello World!</h1> <p>Some random bit of text</p> </div>") (res/content-type "text/html")))
content-typeがtext/htmlになっているのと、bodyに含まれる文字列が実際のhtmlタグを使った文書になっている。とりあえず、現在は最低限のring機能の紹介ということでテンプレートを使っていない。実際のサイト作成ではまずこのようなhtmlタグのベタ書きはせず、テンプレートライブラリを使うことになる。私はfront-endでreagentを使っているので、それに合わせてhtmlテンプレートはhiccupというclojureデータ構造をフル活用するライブラリを使おうと考えている。
not-found関数:
(defn not-found [req] (-> (res/not-found "<b>Not found!</b>") (res/content-type "text/html")))
res/not-found
を使うことによってレスポンスマップは:status 404
になる。やはりこれも現在はタグベタ打ち。
redirect関数:
(defn redirect [req] (res/redirect "http://www.google.com"))
これで問答無用でグーグル先生に飛ぶ。
あとはこれらのハンドラ関数のどれかを
(def my-app (-> plain-text ;swap this with another handler function logger))
のplain-text
のところに代入してreplに送ればlocalhost:8080
にブラウザでアクセスした時の挙動が変わる。
次回はルーティング、つまりuriに指定されたパスによって表示するコンテンツを変えるという機能を付け加えてみる。