The Quote That Escaped the Link — JavaScript Bug Hunt
Modelled on the Twitter "onMouseOver" worm (21 September 2010): a tweet containing a link followed by an @ and a double quote broke out of the href…
- Language: JavaScript
- Layer: Frontend
- Difficulty: Easy
- Concepts: Security, Parsing
- Modelled on: Twitter · 2010
- Visible tests: a plain link is linkified; a double quote cannot close the href
- Reward: 50 XP for a complete fix
Briefing
Modelled on the Twitter "onMouseOver" worm (21 September 2010): a tweet containing a link followed by an @ and a double quote broke out of the href attribute Twitter's auto-linker generated, adding an attribute of the author's choosing to the link. Merely hovering over such a tweet ran script, and self-retweeting versions spread across the site within hours. Twitter said it had fixed the bug the month before, and a later site update had brought it back.
linkify.js turns the URLs in a tweet into links and escapes everything else — but its escaping was written for text between tags, not for attribute values.
Fix linkify so neither the text nor a link can break out of its context.
Bug report
BUG-HOVER · Priority: Critical (XSS) · Reported by: security
linkify(text) returns HTML:
- a URL is "http://" or "https://" followed by non-whitespace characters; each becomes <a href="E">E</a>, where E is the escaped URL
- all other text is escaped
- escaping is the same everywhere: & -> & < -> < > -> > " -> " ' -> '
- so no URL or text can ever close the href attribute or open a tag
Observed: a URL containing a double quote ends the href value early and the rest of the URL is parsed as new attributes on the <a>.
Logs
[render] tweet 25094329857 -> <a href="http://t.co/@"onmouseover="...">
[csp-report] inline event handler executed on twitter.comThe code as shipped
src/tweets/linkify.js (editable)
var URL_RE = /https?:\/\/\S+/g;
function escapeHtml(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
exports.linkify = function (text) {
var out = "";
var last = 0;
var m;
URL_RE.lastIndex = 0;
while ((m = URL_RE.exec(text)) !== null) {
out += escapeHtml(text.slice(last, m.index));
var url = escapeHtml(m[0]);
out += '<a href="' + url + '">' + url + "</a>";
last = m.index + m[0].length;
}
return out + escapeHtml(text.slice(last));
};
Read-only context: src/tweets/CONTEXTS.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.