CTFでjavascript何もわからなくなったのでパズルとして紹介しておきます。
const x = [] const y = x ['admin','admin','__proto__'] = ['admin','admin','__proto__'] const z = x ['1','2','3'] = ['1','2','3'] console.log(x.includes('admin')) // -> true
このようなコードを書くと最後の行の出力がtrueになるのはなんででしょうか?
(Node.js v16.15.1.で検証済み)
解説はもうちょっと下へ
解説
解説していきます。
正直、動作ベースで理解している所があって、javascriptに詳しい人に是非直していただきたいです。
コードを再掲すると
const x = [] const y = x ['admin','admin','__proto__'] = ['admin','admin','__proto__'] const z = x ['1','2','3'] = ['1','2','3'] console.log(x.includes('admin')) // -> true
となりますが、このコード、セミコロンが無いんですよね。
無くても動いちゃうのか…
なので行が終わっているように見せかけて実は終わってないということが起きています。
具体的には
const x = []; const y = x['admin','admin','__proto__'] = ['admin','admin','__proto__']; const z = x['1','2','3'] = ['1','2','3']; console.log(x.includes('admin')); // -> true
こんな感じに解釈されます。
const yとconst zの部分の代入はしても使われないので、
const x = []; x['admin','admin','__proto__'] = ['admin','admin','__proto__']; x['1','2','3'] = ['1','2','3']; console.log(x.includes('admin')); // -> true
実質こんな感じになります。
ここで配列にtupleみたいな感じで添え字を指定していますが、最後のものしか使われません。
動作ベースでは最後のものだけでした。なんででしょうね。誰かNode.jsのソースコードベースで教えてほしい。
(追記:@arkark_さんに教えてもらえました!カンマ演算子によって添え字部分が評価された結果の戻り値が最後の部分となるからです。)
つまりこれは
const x = []; x['__proto__'] = ['admin','admin','__proto__']; x['3'] = ['1','2','3']; console.log(x.includes('admin')); // -> true
のような感じに解釈されます。
__proto__
については図で理解するJavaScriptのプロトタイプチェーン - Qiitaを見てもらうのが一番分かりやすいと思います。
つまり、要素が無かった時に参照する先なのですが、それを上書きしています。
ちょっとわかりやすくするためにxをダンプしてみましょう。
const x = []; x['__proto__'] = ['admin','admin','__proto__']; console.log(x); // -> [] x['3'] = ['1','2','3']; console.log(x); // -> [ <3 empty items>, [ '1', '2', '3' ] ] console.log(x.includes('admin')); // -> true
[ <3 empty items>, [ '1', '2', '3' ] ]
と出てきましたね。
つまり、[ undefined, undefined, undefined, [ '1', '2', '3' ] ]
ということです。
これはx['3']
への代入前にxを見てみると[]
になることからわかります。
__proto__
はいったんおいておいて、xは最初は空の状態なので、そこに3番目に要素を入れると、0,1,2が入っていない状態の配列が出来上がるという訳です。
さて、ここまでくればもうちょっとです。
x.includes('admin')
を実行したときには、雰囲気で処理を考えると
x[0]
はadminかな?x[1]
はadminかな?x[2]
はadminかな?というのを確認していくわけですが、
x[0]
は参照してもundefined
となってしまいます。
ですが、ここで__proto__
を用意していたことが役立ちます。
要素が無かった時に参照する先なので、x[0]
がundefined
であれば、x['__proto__'][0]
を参照しに行くことになります。
よって、x[0] = x['__proto__'][0] = 'admin'
ということになり、結果としてtrueが返されます。
同様にx[1]
もx[2]
も__proto__
が使われるのですが、そのあたりをわかりやすくしたソースコードが以下です。
const x = []; x['__proto__'] = ['admin0','admin1','admin2']; x['3'] = ['1','2','3']; console.log(x); // -> [ <3 empty items>, [ '1', '2', '3' ] ] console.log(x[0]); // -> admin0 console.log(x[1]); // -> admin1 console.log(x[2]); // -> admin2 console.log(x[3]); // -> [ '1', '2', '3' ] console.log(x.includes('admin')); // -> true
emptyのはずなのに具体的に取得すると値が出てくるんですね。恐ろしい。
よって、いろんなことが起こって最終的にadminが含まれていることになってしまう訳です。
終わりに
ほんとに動作ベースでブラックボックス的に理解しただけなので、間違ってたら指摘してもらえると自分が助かります。
良いお盆休みを。