JavaScriptやejsでエスケープを実現する方法としてencodeURIComponent()
や<%= %>
があるが、その挙動を正確には理解していなかったので、調べてみた。
まず、具体的にどんな機能なのかを調べ、次に、どのようなケースで使うのかを確認する。
機能
encodeURI(),encodeURIComponent()
JavaScriptにビルトインされている関数で、文字列をエンコードしてくれる。
また、これと対になるdecodeURI()
やdecodeURIComponent()
という関数もあり、こちらは、デコードしてくれる。
encodeURI()
もencodeURIComponent()
も使い方は同じで、引数に文字列を与えると、その文字列をエンコードして返す。
違いは、対象となる文字列。encodeURIComponent()
は、より多くの文字列を対象としている。
具体的には以下のとおり。
str | encodeURI(str) | encodeURIComponent(str) |
---|---|---|
, | , | %2C |
: | : | %3A |
; | ; | %3B |
/ | / | %2F |
+ | + | %2B |
= | = | %3D |
? | ? | %3F |
& | & | %26 |
\ | %5C | %5C |
< | %3C | %3C |
> | %3E | %3E |
%20 | %20 | |
あ | %E3%81%82 | %E3%81%82 |
" | %22 | %22 |
' | ' | ' |
あくまでも自分が調べた範囲の話なので、これ以外にも対象となる文字列はあるかもしれない。
シングルクォーテーションは、どちらの関数でも対象外であり、素通りする。
decodeURI()
とdecodeURIComponent()
は、上記の逆パターンと考えればいい。
だが、シングルクォーテーションがデコードの対象になる、という違いはある。
var str = '%27'; // %27は'にデコードされるが…… console.log(decodeURI(str)); // ' console.log(decodeURIComponent(str)); // ' str = '\''; // その逆は行われず、'のまま console.log(encodeURI(str)); // ' console.log(encodeURIComponent(str)); // '
<%= %>
テンプレートエンジンであるejs。
動的にhtmlを出力するのに使われたりする。
<p> <%= keyword %>の検索結果 </p>
このように記述することで、例えばユーザーの入力内容に応じてhtmlの内容を動的に生成することが出来る。
<%= %>
を使うとエスケープされ、<%- %>
を使うとエスケープされずに出力される。
<p><%= script %></p> <p><%- script %></p>
上記のようなejsに、script
に対して<script>hoge</script>
を与えると、下記のように出力される。
<p><script>hoge</script></p> <p><script>hoge</script></p>
主なエスケープ対象は、以下。
エスケープ前 | エスケープ後 |
---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
先ほどのJavaScriptのエンコードと異なり、シングルクォーテーションも対象となる。
その一方で、半角スペースやバックスラッシュは対象ではない。
使用例
それではどのようなケースで、エンコードやデコード、エスケープが必要になるのか。
URLエンコード
URLに使用できる文字には制限があり、それ以外の文字、例えば日本語を使用すると、パーセントエンコードされる。
そのため、URLの文字列をプログラムに使用する際、デコードする必要のあるケースが出てくる。
例えば、ユーザーからの入力をクエリとして渡すとする。
/search?q={ユーザーからの入力値}
あ
が入力されると、そのURLは/search?q=あ
となるが、エンコードされてしまうため実際には/search?q=%E3%81%82
となる。
これをあ
として復元するために、デコードを行う。
var http = require('http'); var server = http.createServer(); // http://localhost:8080/test/search?q=あ // にアクセス server.on('request', function(req, res){ res.writeHead(200, {'Content-Type': 'text/html'}); console.log(req.url); // /search?q=%E3%81%82 console.log(decodeURI(req.url)); // /search?q=あ console.log(decodeURIComponent(req.url)); // /search?q=あ res.end(); }); server.listen(8080);
デコードすることで、日本語として扱えていることが分かる。
後は、この日本語を使って、検索なり表示なりを行えばよい。
XSS
エスケープが最も必要となるのが、XSS(クロスサイト・スクリプティング)対策だと思う。
XSSとは、攻撃者の作成した任意のコードを埋め込まれてしまう脆弱性。
詳しくは下記のページなどを参照。
第2回 Webセキュリティのおさらい その2 XSS:JavaScriptセキュリティの基礎知識|gihyo.jp … 技術評論社
クロスサイトスクリプティング対策 ホンキのキホン - 葉っぱ日記
XSS対策としてエスケープが必要となるケースは、何種類かある。
ユーザーから受け取った値をテキストとして表示するケース
例えば、サイト内で検索を行い、その結果のページで「◯◯の検索結果」と表示させるようなケース。
エスケープを行わないと、任意のJavaScriptコードを実行されてしまう。
ユーザーが検索フォームにプログラミング
と入力した場合、URLはhttp://example.jp/search?q=プログラミング
となり、以下のようなhtmlが生成されるとする。
実際には上述のエンコードやデコードが行われるが、そこは本質ではないので省略する。以下の例でも同じ。
<span>プログラミング</span>の検索結果
これなら何も問題はない。
だが、ユーザーが<script>alert('attack');</script>
と入力すると、どうなるか。
URLはhttp://example.jp/search?q=<script>alert('attack');</script>
となり、htmlの中身は以下のようになる。
<span><script>alert('attack');</script></span>の検索結果
ユーザーが入力したコードがページに埋め込まれた形になっており、ページが読み込まれた瞬間、実行されてしまう。
適切なエスケープを行うことで、このような事態は防げる。
例えば、先程説明したejsのエスケープ機能を使う。
<span><%= ここにユーザーからの入力値 %></span>>
そうすると、生成されるhtmlはこのようになる。
<span><script>alert('attack');</script></span>
この場合、ユーザーからの入力値は(コードではなく)単なる文字列として扱われるため、XSSは発生しない。
ユーザーから受け取った値を属性値として出力するケース
ユーザーがプログラミング
と入力すると、以下の様なhtmlを生成するケース。
<input type="search" name="q" value=プログラミング>
ユーザーがx onmouseover=alert("attack")
と入力すると、以下の様なhtmlが生まれてしまう。
<input type="search" name="q" value=x onmouseover=alert("attack")>
こうなると、このinput
要素にマウスオーバーした瞬間、攻撃者が用意したコードが実行されてしまう。
これは、ejsのエスケープでは防げない。エスケープしても下記のようなhtmlになり、やはり、マウスオーバーするとコードが実行されてしまう。
<input type="search" name="q" value=x onmouseover=alert("attack")>
JavaScriptのエンコードを使えば、防ぐことが出来る。それぞれ下記のようなhtmlになるため、コードは無力化される。
<!-- encodeURI() --> <input type="search" name="q" value=x%20onmouseover=alert(%22attack%22)> <!-- encodeURIComponent() --> <input type="search" name="q" value=x%20onmouseover%3Dalert(%22attack%22)>
だがそもそもこのケースでは、属性値をクォーテーションで囲っていないのが問題である。
今回のケースにおいては、クォーテーションで囲っておけば、エンコードしていなくても問題は発生しなかった。
<!-- クォーテーションで囲えば、スクリプトを無害化できた --> <input type="search" name="q" value="x onmouseover=alert("attack")"> <input type="search" name="q" value="x onmouseover=alert('attack')">
ユーザーから受け取った値を属性値として出力するケース その2
ではクォーテーションで囲っておけばエスケープしなくていいのかというと、そうではない。
クォーテーションで囲っていても、適切にエスケープしておかないと、XSSは発生しうる。
ユーザーがプログラミング
を入力した結果。
<input type="search" name="q" value="プログラミング">
今回はクォーテーションで囲まれている。
ではこのケースでユーザーが" onmouseover="alert('attack')
と入力したらどうなるか。
<input type="search" name="q" value="" onmouseover="alert('attack')">
このようなhtmlが生成され、XSSが発生してしまっている。
これは、クォーテーションをエスケープしていなから発生してしまった。
ejsでエスケープすれば以下の様なhtmlになり、コードが埋め込まれることはない。
<input type="search" name="q" value="" onmouseover="alert('attack')">
リンクを動的に生成するケース
リンクを動的に生成するようなページにおいて、リンク先をJavaScriptにされ、クリックすると任意のコードが実行されてしまう、というパターンのXSSもある。
例えば以下のhtmlでは、リンクをクリックすると、コードが実行される。
<a href="javascript:alert('attack')">リンク</a>
これは、ejsのエスケープでは防げない。
エスケープの結果は以下の様になり、コードは有効なままである。
<a href="javascript:alert('attack')">リンク</a>
encodeURI()
でも防げない。
<a href="javascript:alert('attack')">リンク</a>
encodeURIComponent()
なら、:
がエスケープされ、無害化できる。
<a href="javascript%3Aalert('attack')">リンク</a>
だが根本的には、リンクを動的に生成する場合は、その先頭がhttp://
などの安全な文字列で始まっているかを確認したほうが堅牢である。