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: & -> &amp; < -> &lt; > -> &gt; " -> &quot; ' -> &#39;
  • 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.com

The code as shipped

src/tweets/linkify.js (editable)

var URL_RE = /https?:\/\/\S+/g;

function escapeHtml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

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.