Challenge

A Node/Express ticketing app. A /debug route conveniently served its own source. The /add-ticket handler deep merged the request body into a ticket object with a recursive assign, and /admin checked req.user.admin === true.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const assign = (target, source, merged = new Set()) => {
  ...
  for (let key in source) {
    if (isObject(target[key]) && isObject(source[key])) {
      assign(target[key], source[key], merged);
    } else {
      target[key] = source[key];
    }
  }
  return target;
};
app.post("/add-ticket", (req, res) => {
  const ticket = { id: crypto.randomUUID(), title, issue };
  assign(ticket, req.body);   // merges attacker JSON, no key filtering
  ...
});

Approach

The recursive merge walks for (let key in source) with no guard on __proto__, so a crafted ticket body pollutes Object.prototype with admin: true. After that, the req.user.admin === true check on /admin passes and the admin page (and a debug/dev shell from there) is reachable.