読者です 読者をやめる 読者になる 読者になる

ぶれすとつーる

だいたいjavascript

文字列リテラルと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であることが起因してる

es5 #7.8.4

es5 #7.3


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が適応されていてそっちにはその制約がないから