暮らしの技術

暮らしを豊かにする技術や、特に暮らしを豊かにしない技術があります

jQueryの.each()に潜む罠

.each()の話

jQueryの.each()を使うと,JavaScriptのオブジェクトか配列を対象にした繰り返しを簡単に記述できる.*1

var list = [0,1,1,2,3,5,8,13];
$.each(list, function(index, elem){
  console.log(elem * 2);
});
// 0 VM2035:4
// 2 VM2035:4
// 2 VM2035:4
// 4 VM2035:4
// 6 VM2035:4
// 10 VM2035:4
// 16 VM2035:4
// 26 VM2035:4

で,このeachにはもう一つ書き方がある.

var list = [0,1,1,2,3,5,8,13];
$.each(list, function(){ // さっきは引数にindex, elemを指定していた
  console.log(this * 2); // 引数ではなく,thisでアクセスしている
});
// 0 VM2032:4
// 2 VM2032:4
// 2 VM2032:4
// 4 VM2032:4
// 6 VM2032:4
// 10 VM2032:4
// 16 VM2032:4
// 26 VM2032:4

違いは?

ドキュメントにはこうある.

The value can also be accessed through the this keyword, but Javascript will always wrap the this value as an Object even if it is a simple string or number value.

http://api.jquery.com/jQuery.each/

値にはthisキーワードを使用してもアクセスできるが,Javascriptはそれが単なる文字列や数値であっても常にObjectとしてこの値をラップする

jQueryのコードで言うとhttps://github.com/jquery/jquery/blob/master/src/core.js#L279のあたり.

これで特に大きな変化が起きるのが要素にnullやundefinedが含まれていた場合で,ブラウザで実行される場合thisがwindowとなる.*2

問題になるシナリオ

ある学校で,部活動とその部に所属している学生の名前を対話的に表示するウェブページを実装する場面を考える.

f:id:side_tana:20140908211714p:plain

雑にはこういう感じ.

表示をスムーズに切り替えるため,このページではアクセス時に全学生のデータと,全部活動のデータをサーバから取得してる*3.それぞれの例を以下に示す.

var clubMembers = {
         'cl001': ['st13010', 'st13011', 'st14012'],
         'cl002': ['st13100', 'st14100', 'st14005'],
         'cl003': ['st12023', 'st13045', 'st14009']
};
var students = {
         'st13010': {'name': 'A', 'grade': 2, 'class': 'A', 'sex': 'F'},
         ...
         'st14009': {'name': 'I', 'grade': 1, 'class': 'A', 'sex': 'F'}
};

*4

名簿の表示について,ここでは先に詳細な学生リストをつくり,それを利用する方針で実装してみる.

var clubMembersDetail = function(clubName){
	var detail = [];
	$.each(clubMembers[clubName], function(){
		detail.push(students[this]);
	});
	return detail;
};

// ...

$('#clubSelector').on('change', function () {
	var clubName = this.value;
	$.each(clubMembersDetail(clubName), function(){
		$('ul#memberlist').append('<li>' +  this + '</li>');
	});
});

さて,ここでcl001のst14012が転校しており,すでに学生の名簿には無いとどうなるか.clubMembersDetailの3行目students[this]でundefinedとなるため,リストは[<student obj>, <student obj>, undefined]となり,表示は以下のようになる.

f:id:side_tana:20140908211816p:plain

ここで,最後の空文字を出力している時のthisはwindowであり,つまりwindow.nameが""であることを出力している.window.nameは外部から書き換えが可能なため,システムの出力を信じてappendしている今回のケースではXSSにつながる.

f:id:side_tana:20140908212852p:plain

ということで

最後は明らかに悪意のある例になってしまったけど*5,要素に値を入れるような場合では新たにwindowのプロパティを定義してしまったり,最悪の場合すでに定義されているwindowのプロパティを破壊することになるし,参照の例でもnullやundefinedを期待してcallbackの中でthis === nullやthis === undefinedのような条件を書いてしまうとうまく動かないことになる.

以上になります.お気をつけください.

追記

gistに置きましたので興味がある人は遊んでみてください.


jQueryの.each()のやつ

*1:$(selector).each()ではなく$.each()の方のお話.

*2:これ結局Function.prototype.call()とFunction.prototype.apply()の第一引数にnull/undefinedを渡したときの仕様ってことなんですかね? ちょっとそこまで調べてないです.

*3:いやそれは無いだろって思うかも知れないけどとりあえずそれは置いておいて

*4:フォーマットはstはstudent,次の二桁が入学年度,末尾の3桁がその年度での名前順ということにしてみよう.

*5:appendを使わなければとりあえずXSSは避けれられる