SourCode a FreeFungi app

Challenges FF-010

Liquorice CWE-1321 A08:2021

Wrapped Wrong

Prototype pollution in the saved cart.

The shop can save your bag to a cookie and merge it back in later. That merge walks the saved object key by key and copies it in, and it does not stop at a special key called __proto__. Slip that key into a saved bag and you are not editing your bag any more, you are editing the blueprint that every object in the app is built from. This is prototype pollution.

New to this? What is prototype pollution?

In JavaScript, objects inherit properties from a shared prototype. If you can set a property on that prototype, it appears on every object at once, even ones created later.

Prototype pollution is when untrusted data is merged into an object without guarding the magic keys __proto__, constructor and prototype. Pollute a property the app reads elsewhere and you can flip a flag, grant a discount, or bypass a check for the whole process.

Make sure the shop is running at http://localhost:3000. You will edit a cookie, so open your browser’s developer tools (right-click the page and choose Inspect, or press F12 on Windows / Cmd+Option+I on a Mac). No proxy needed.

1 · Find it

Add a sweet to your bag, open the bag, and click Save bag for later. That sets a cookie called ff_saved. Find it in dev tools: the Application tab (Chrome/Edge) or Storage tab (Firefox) → Cookies → click http://localhost:3000 → the row named ff_saved.

Its value is base64 (a plain, reversible way of packing text, not encryption). Decode it with the free website CyberChef (paste the value in, drag a From Base64 block into the recipe), or in a Mac/Linux terminal:

echo 'eyIxMiI6eyJxdHkiOjEsInVuaXRfcHJpY2VfcGVuY2UiOjE1MH19' | base64 -d

Out comes your bag as JSON, an object the shop will happily merge straight back in when you restore it.

2 · Exploit it

The cart applies a discount from an internal settings object that is normally empty. If you can sneak a discount_pence onto every object at once, the cart will use it. You do that by adding a special key, __proto__, to your saved bag. This is the text to smuggle in:

{"__proto__":{"discount_pence":100000}}

Encode that back to base64 (CyberChef: a To Base64 block; or the terminal line below), which gives you eyJfX3Byb3RvX18iOnsiZGlzY291bnRfcGVuY2UiOjEwMDAwMH19:

printf '{"__proto__":{"discount_pence":100000}}' | base64

Now put that value in the cookie: in dev tools under Cookies, double-click the value of ff_saved, paste your encoded string, press Enter. Then visit http://localhost:3000/bag/restore and open the cart.

A discount you were never offered has appeared, and the total has collapsed to £0.00. The pollution sticks for the whole running app, not just your session (restart the container to clear it).

Why it matters: the same trick flips isAdmin, disables checks, or plants a gadget that turns into remote code execution in the right codebase. It is quiet and it is global.

3 · Understand it

The merge recurses into nested objects and, when the key is __proto__, follows it straight onto the shared prototype:

function merge(target, source) {
  for (const key of Object.keys(source)) {   // includes "__proto__"
    if (source[key] && typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);        // target["__proto__"] is Object.prototype
    } else {
      target[key] = source[key];
    }
  }
}

4 · Fix it

Refuse the dangerous keys, and build the parsed object without a prototype so there is nothing to climb onto:

✗ Vulnerable
for (const key of Object.keys(source)) {
  // copies __proto__ too
  ...
}
✓ Patched
const BAD = ['__proto__','constructor','prototype'];
for (const key of Object.keys(source)) {
  if (BAD.includes(key)) continue;
  ...
}

Reaching for a vetted merge (or structuredClone, or Object.create(null) targets, or a schema that only allows known keys) is safer than hand-rolling one.

The general rule: when you copy untrusted data into an object, never let it name __proto__, constructor or prototype. Validate against a known shape instead of merging whatever arrives.