The Invisible Tweet Button — JavaScript Bug Hunt

Modelled on the Twitter "Don't Click" clickjacking worm (February 2009): a page showed a single button labelled "Don't Click" over an invisible frame of…

  • Language: JavaScript
  • Layer: Frontend
  • Difficulty: Easy
  • Concepts: Security, Config
  • Modelled on: Twitter · 2009
  • Visible tests: API responses are untouched and login stays protected; the tweet page cannot be framed
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Twitter "Don't Click" clickjacking worm (February 2009): a page showed a single button labelled "Don't Click" over an invisible frame of Twitter's own page. Signed-in visitors who clicked it actually clicked Twitter's update button, posting the same link from their own account and spreading it to their followers. Twitter responded by stopping its pages from being framed by other sites.

This project is a reconstruction. headers.js builds response headers, but it only adds frame protection to a hand-picked list of "sensitive" routes — and the page that posts a tweet is not on it.

Fix buildHeaders so no HTML page can be framed by another site.

Bug report

BUG-TW-0212 · Priority: Critical · Reported by: security

buildHeaders(path, contentType) returns a header object that always has Content-Type = contentType. For EVERY HTML response (contentType starting with "text/html"), on any path, known or not:

  • X-Frame-Options: "DENY"
  • Content-Security-Policy: the route's own policy (routes.ROUTES[path].csp) followed by "; frame-ancestors 'none'", or just "frame-ancestors 'none'" when the route has none

Non-HTML responses keep the route's CSP if any and get no X-Frame-Options.

Observed: /home — the page with the tweet box — can be framed by any site.

Logs

[edge] GET /home referer=http://evil.example/dontclick.html sec-fetch-dest=iframe 200
[statuses] burst of identical "Don't Click" updates from unrelated accounts

The code as shipped

src/web/headers.js (editable)

var routes = require("./routes");

var FRAME_SENSITIVE = ["/login", "/settings"];

exports.buildHeaders = function (path, contentType) {
  var h = { "Content-Type": contentType };
  var route = routes.ROUTES[path] || {};
  if (route.csp) h["Content-Security-Policy"] = route.csp;
  if (FRAME_SENSITIVE.indexOf(path) !== -1) h["X-Frame-Options"] = "DENY";
  return h;
};

Read-only context: src/web/routes.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.