One Domain Checked Three Times — JavaScript Bug Hunt

Modelled on the Let's Encrypt CAA rechecking bug (found 29 February 2020): when a certificate request covered several domain names whose CAA records needed…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Security, Concurrency, Validation
  • Modelled on: Let's Encrypt · 2020
  • Visible tests: a single-name order is checked; every name is checked once
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Let's Encrypt CAA rechecking bug (found 29 February 2020): when a certificate request covered several domain names whose CAA records needed rechecking, Boulder — Let's Encrypt's CA software — checked one of those names N times instead of each name once. The Go code captured a reference to the loop variable, so every check saw the same name. Let's Encrypt announced it would revoke about 3 million affected certificates.

caa.js reconstructs it in JavaScript: the rechecks are queued as closures inside a for loop and run afterwards by pool.js, the way Boulder fanned them out.

Fix recheck so every name on the order is checked exactly once.

Bug report

BUG-CAA-2020 · Priority: Critical (mis-issuance) · Reported by: incident response

recheck(domains, lookup) — lookup(domain) returns true when CAA permits issuance.

  • lookup is called exactly once for each entry of domains, with that entry, in input order (duplicates in the input are each checked)
  • returns { ok, failed }: failed lists the domains whose lookup returned false, in input order; ok is true exactly when failed is empty
  • an empty list -> { ok: true, failed: [] }

Observed: for ["a.example", "b.example", "c.example"] lookup was called with "c.example" three times; a.example and b.example were never checked.

Logs

[boulder] recheckCAA order=4418 names=3
[boulder] caa lookup c.example ok
[boulder] caa lookup c.example ok
[boulder] caa lookup c.example ok

The code as shipped

src/ca/caa.js (editable)

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

// Re-checks CAA for every name on an order before issuance.
exports.recheck = function (domains, lookup) {
  var tasks = [];
  for (var i = 0; i < domains.length; i++) {
    var domain = domains[i];
    tasks.push(function () {
      return { domain: domain, ok: lookup(domain) };
    });
  }
  var results = pool.runAll(tasks);
  var failed = results
    .filter(function (r) { return !r.ok; })
    .map(function (r) { return r.domain; });
  return { ok: failed.length === 0, failed: failed };
};

Read-only context: src/ca/pool.js.

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