文字列リテラルとU+2028
元気にインターネットしてたらユーザの入力した値をこんな感じでDOMに埋め込んでるサイトをみつけた
<a href='javascript:edit(2,{"a":"どらえもん","b":["×","×"],"c":"ふごふご"},1)'> ほげええ </a>
入力値いろいろかえて遊んでみたらだいたいの文字列はエスケープはされてたけど曰くのU+2028はsyntax errorだしてた
Uncaught SyntaxError: Unexpected token ILLEGAL
これは文字列リテラルの仕様で含められない値として決められてるLineTerminatorがu+2028であることが起因してる
7.8.4 String Literals
DoubleStringCharacter :: SourceCharacter but not double-quote " or backslash \ or LineTerminator \ EscapeSequence SingleStringCharacter :: SourceCharacter but not single-quote ' or backslash \ or LineTerminator \ EscapeSequence
7.3 Line Terminators
Code Point Value | Name | Formal Name |
---|---|---|
\u000A | Line Feed | <LF> |
\u000D | Carriage Return | <CR> |
\u2028 | Line separator | <LS> |
\u2029 | Paragraph separator | <PS> |
今回のこの問題をかんたんに再現したいならhrefのjavascript:疑似プロトコル中にu+2028が文字列リテラルに含めまれるようにして、あとはクリックイベントをエミュレートすればいい
検証コード
<a id="fuga" href="#">fugaa</a>
function fn(obj) {} document.addEventListener('DOMContentLoaded', function () { var fuga = document.getElementById('fuga'), invalid = String.fromCharCode(0x2028), evt; fuga.setAttribute('href', 'javascript: fn({"a": "' + invalid + '"})'); // クリックイベントをエミュレート evt = document.createEvent("MouseEvents"); evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); fuga.dispatchEvent(evt); // Uncaught SyntaxError: Unexpected token ILLEGAL // (クリックされたタイミングでjavascipt文字列を評価するから) }, false);
とうぜん(javascript文字列中の)文字列リテラルにu+2028を含めてevalしても同じことがおこる
var invalid = String.fromCharCode(0x2028), code = "console.log('" + invalid + "')"; eval(code); // SyntaxError: Unexpected token ILLEGAL
new Function系も当然だめ
var invalid = String.fromCharCode(0x2028), code = "console.log('" + invalid + "')"; new Function(code); // SyntaxError: Unexpected token ILLEGAL
json文字列をevalで評価するときにもいったんstringifyされた段階で生成されるinvalidな文字列リテラルがevalで評価されちゃうのでエラーがおきる
var invalid = String.fromCharCode(0x2028), j = JSON.stringify({a: invalid}); // {a: "<LS>"} が生成されて次の行でそれをevalされるのでNG eval('(' + j + ')'); // SyntaxError: Unexpected token ILLEGAL
というわけでJSON.parseつかおう(変換時にリテラルを生成しないから問題がおきない)
今回の件でいえばdata-*で取得するのが多分一番いい
それでもやっぱりまたそれらを文字列化して評価される恐れがないわけではないのでそういうのがありえる処理系なら空文字なりに置換しちゃいたいところ
ちなみにjson自身はなんでu+2028を含むリテラルが正常に扱えるかというとjsonはjsの仕様(ecma262)とは別で標準化されててecma404が適応されていてそっちにはその制約がないから