An Easy-rated room from TryHackMe’s “Love at First Breach 2026” Valentine’s event — a gift-shop web app that trusts a role claim baked straight into the client’s own JWT cookie.
Room brief
A “find the hidden item” prompt on an e-commerce-styled app almost always means one of two things: a hidden route, or an access-control check that trusts something it shouldn’t. Turned out to be the second.
Step 1 — Poking around the shop
The app is a small Valentine’s-themed storefront: roses, heart chocolates, chocolate-dipped strawberries, and a love letter card, each with a credit price. No “ValenFlag” item visible anywhere in the normal listing.
Creating an account is free and doesn’t need verification, so signed up with a throwaway address to get past the “Guest” wall and see what a logged-in session looks like.
After logging in, the account page showed the expected starting state:
Zero credits, no obvious hidden item, and no way to buy anything even if there were one. Time to look at how the app actually tracks who’s logged in and what they’re allowed to do.
Step 2 — Finding the session cookie
Opened DevTools and checked storage. The app sets a single cookie to track the session:
Name: tryheartme_jwt
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. ...That eyJhbGciOi... prefix is the unmistakable signature of a Base64url-encoded JWT header. Three dot-separated segments - header, payload, signature - meaning the app's entire idea of "who you are" lives in a token sitting in a cookie I fully control the storage of.
Step 3 — Decoding the token
Pasted the full cookie value into jwt.io’s decoder to see what’s actually inside it.
Decoded payload:
{
"email": "lame@mail.com",
"role": "user",
"credits": 0,
"iat": 1794288323,
"theme": "valentine"
}And there it is — "role": "user", sitting in plain, readable JSON inside a cookie that lives entirely on the client. If the server trusts this claim without re-checking it against something server-side, then the only thing standing between this account and an admin one is the token's signature.
Step 4 — Forging an admin token
Header confirms the signing algorithm:
{ "alg": "HS256", "typ": "JWT" }HS256 is symmetric - the same secret key both signs and verifies the token. If that secret is weak, default, or otherwise guessable/crackable (a very common misconfiguration in small apps like this one), anyone who recovers it can mint their own, fully valid, arbitrarily-privileged tokens from scratch.
Get Chetan Patil’s stories in your inbox
Join Medium for free to get updates from this writer.
Switched to jwt.io’s JWT Encoder tab, kept the same payload structure, and changed one field:
{
"email": "lame@mail.com",
"role": "admin",
"credits": 0,
"iat": 1794288323,
"theme": "valentine"
}Signed it with the matching secret and got back a freshly forged, validly-signed JWT with my own custom claims baked in.
Step 5 — Swapping the cookie
Replaced the browser’s tryheartme_jwt cookie value with the newly forged token:
Reloaded the account page:
Two things changed at once:
- A brand-new Admin link appeared in the navigation, confirming the server is reading the
roleclaim straight out of the JWT to decide what UI/routes to expose. - Credits jumped from
0to5000- the server evidently grants staff/admin sessions a fixed credit allowance server-side, rather than trusting whatever number happens to be in the token'screditsfield.
Step 6 — Admin portal and the hidden item
The Admin link leads straight to a staff-only area:
And there’s the hidden item the room brief was asking about — ValenFlag, gated entirely behind that role claim and otherwise invisible in the normal shop listing.
With the forged admin session carrying 5000 credits — comfortably more than the 777-credit price tag — purchased it directly.
Step 7 — Flag
The purchase completes and prints a receipt with a redeemable voucher:
Receipt
Order: ValenFlag - 777 credits
Voucher: ValenFlag redeemed
THM{v4l3nt1n3_jwt_c00k13_t4mp3r_4...}Root cause
This is a textbook JWT trust boundary failure, stacked two deep:
- Client-controlled authorization claims.
role(andcredits) are stored directly in a token the client holds and can freely decode. Storing authorization-relevant data in a JWT is fine only if the signature is the sole thing standing between a user and forging it - which leads straight to the second issue. - A weak or recoverable signing secret. HS256 is only as strong as the secret backing it. If that secret can be guessed, brute-forced, or was ever leaked (hardcoded in source, a common default, a short/low-entropy string), the token’s signature stops being a meaningful trust boundary at all — anyone can mint whatever claims they like and sign them validly.
Room Completed!





![[THM] -Brr — ScadaBR writeup](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*guqO8YC1Jq1d5dhiY2iKGg.png)


