Merry X(ma)SS (Shanon Advent Calender 2016 - 25日目)
こんにちは,こんばんは,メリー・クリスマス,開発エンジニャーの t です.
顔認識から始まり,テスト,PM,子育て,開発プロセス,VR, AWS, RoboHon, Golang etc. と多種多様なテーマを技術部他総出演で展開してきた Shanon Advent Calender 2016 も無事最終日となりました.
最終日,25日は Merry XmaS と掛けて 2016年に出会った XSS の話で締めたいと思います.
はじめに
ひとくちに XSS といっても色々有りますが,主に DOM based XSS と呼ばれるクライアントサイド(ブラウザ)で起こるタイプの XSS の話をしていきます.体系的な原因と対策等については参考文献のリンク先を参照してください.
最近の傾向(肌感覚)
2016年の傾向としては,意外なことに document.write
を経由するパターンとの遭遇率が高かったです.
要因としては,
- jQuery を始めとしたライブラリ,フレームワークの更新によって起こりにくくなってきた
- 一方で valila js への間違った回帰が起きている
- 世間的にPCとWeb(ブラウザ)からモバイル端末と(ネイティブ)アプリへリソースが移動してタコツボ化している
- 一方で古いモノのケアが取り残されている
- そもそも自分の観測範囲は狭くて,そういうものばかり見つけてしまっている
などなど思いつく要因はいくつかありますが,どれも真面目に考察できるほどの根拠はないです.
事例紹介
あくまで事例としてなので具体的なサービス名等は伏せておきます.
ケース 1 URL にハイライトを記録する
一つ目の事例は,location.href
からパラメータを取り出して setTimeout()
用の function
を作ってしまっていた事例です.
このサービスではFlashを用いて電子書籍的なサービスを展開していて,location.search
部分に閲覧中のページ番号を残せるようになっていました.
さらに,ページ内の四角形の選択領域の情報も location.search
に残せるようになっていました.
具体的には,http://ebook.example.com/book/42?page=20&area=100;100;200;200
と言うようなURLになっていていました.
この例では 20ページ目を開いて,(画面左上を原点として)(100, 100) から (200, 200) を網掛け(ハイライト)をするという指定になります.
実際の利用するコードは以下のようになっていました(変数名など適宜調整しています).
var arrRequestVar;
function initRequestVar {
var strHref=document.location.href;
if(strHref.lastIndexOf("?")>0){
strHref = strHref.substring( strHref.lastIndexOf("?")+1 );
var arr_1 = strHref.split("&");
for(var i=0; i<arr_1.length;i++){
var arr_2 = arr_1[i].split("=");
if(arr_2.length==2){
var oParam = {name:arr_2[0].toLowerCase(),value:arr_2[1]};
arrRequestVar.push(oParam);
}
}
}
}
function getRequestVar(name){
var ret='';
for(var i=0; i<arrRequestVar.length;i++){
if(arrRequestVar[i].name==name.toLowerCase()){
ret=arrRequestVar[i].value;
break;
}
}
return ret;
}
function setHighlight(page, x1,y1,x2,y2){
swfobjct.handle_setFocusArea(false, page, x1,y1,x2,y2, "blink=1&borderThick=3&bgColor=#EEEEEE", true, false);
}
というようなライブラリ部分があり,ページ読み込み時に以下のようなコードが実行されるようになっていました
try{
var page=parseInt(getRequestVar("page"));
if(page>0){
var arr=getRequestVar("area").split(";");
if(arr.length==4){
setTimeout("try{setHighlight("+page+","+arr[0]+","+arr[1]+","+arr[2]+","+arr[3]+");}catch(ae){}",500);
}
}
}catch(e){}
このコードには幾つもの問題がありますが,今回の問題に限ると setTimeout
に function
ではなく,無検証・無加工のユーザ入力から組み立てた文字列を渡していることが最大の問題でした.
クエリ部分の取り出しに document.location.href.lastIndexOf('?')
を使っていることも重要度は低いですが,やめたほうが良いところです.
location,hash
が混ざることを始め,理由は挙げればげればキリがありませんが. locatoin.search
で事足ります.
location.search
の key=value
の部分の処理に関してもいくらでも語れることはありますが,参考文献の2 を参照ください.
解決策1
以下のように function
を渡すようにしてあれば,整数値への制限をしなかったとしても問題は回避できていました.
ただし,このコードではsetTimeout
で待っている 500 ms の間に arrRequestVar
が書き換えられてしまう可能性に対応できていないので,最初の問題のあるコードとはやや挙動が異なってしまっている点に注意が必要です.また setTimeout(trySethighlight, 500, page, arr[0], arr[1], arr[2], arr[3])
とすると実行タイミングの問題は解消出来ますが IE は 10 以上からの対応なのでこちらも注意が必要です.
setTimeout(function trySetHighlight(){
try{
var page = parseInt(getRequestVar('page'));
if (page>0){
var arr = getRequestVar('area').split(';');
if (arr.length === 4){
try{
setHighlight(page,arr[0],arr[1],arr[2].arr[3]);
}
catch(e){}
}
}
}catch(e){}
}, 500);
解決策 2
setHighlight
が呼び出すFlashのメソッドでは数値以外は扱わないようになっているため,もしこれが以下のように,文字列から組み立てるとしても area
の方も parseInt
を使って整数値(自然数)に制限してしまえば,問題は起こりませんでした.
try{
var page=parseInt(getRequestVar("page"),10);
if(page>0){
var arr=getRequestVar("area").split(";");
if(arr.length==4){
var ng=0;
for(var i=0;i<arr.length;i++){
arr[i]=parseInt(arr[i],10);
if(arr[i]<0){ ng = 1; break; }
}
if (!ng) {
setTimeout("try{setHighlight("+page+","+arr[0]+","+arr[1]+","+arr[2]+","+arr[3]+");}catch(ae){}",500);
}
}
}
}catch(e){}
ただし,これは数値を渡すという今回の場合にのみ使える方法で,文字列を渡す関数を呼ぶ場合には更に複雑な制限とエスケープが必要になります.
まとめ
location.href
からパラメータを取り出して setTimeout に文字列として与えてしまう事例を紹介しました- setTimeout に関数を渡す場合の対応策と注意点を紹介しました
- 文字列で渡さざるを得ないの場合の限定的な対応策を紹介しました
参考文献
- [JavaScriptセキュリティの基礎知識:連載|gihyo.jp … 技術評論社](http://gihyo.jp/dev/serial/01/javascript-security/)
- [Node v7 で入った WHATWG URL 実装について](https://blog.jxck.io/entries/2016-10-27/whatwg-url.html)