Technique · sanitized
Cookie Value Manipulation
Here is the one sentence the whole thing rests on: a cookie is data your server sent to a computer you do not control, and it will hand you back exactly what it decides to. Every cookie is a round trip through the user’s browser, and the browser is a place where the value can be read, decoded, changed, and sent back looking untouched. If your application trusts what comes back, you have moved a security decision onto the attacker’s machine.
This is one of the first things worth learning well, because it is everywhere and the fix is conceptual, not a library you bolt on.
Sanitized · lab-safe
This is a technique explainer, not a target write-up. There is no live system described anywhere on this page — every cookie, value and role string below is a generic, lab-safe illustration of a class of flaw, published so you can learn to build against it. No flag appears here.
The habit it teaches is defensive: when you ship something, ask of every value the client hands back, “what happens if they changed this?” If the honest answer is “something bad,” the value belongs on the server.
Section 1
What lives in a cookie
Sort every cookie into two piles · only one is dangerous
The two piles · Opaque handles vs meaningful values
Which parts are dangerous
Open DevTools → Application → Cookies on any site and you will see rows like these:
| Cookie | Value | What it is |
|---|---|---|
session | 9f2a1c… | opaque, long, random |
theme | dark | meaningful |
cart | %7B%22id%22%3A42%7D | meaningful (URL-encoded JSON) |
role | user | meaningful — carries its own authority |
Sort them into two piles:
- Opaque handles.
session=9f2a1c...is a pointer. It means nothing on its own; the server looks it up in its own store to find out who you are. Changing it just points at nothing (or, if you can guess a valid one, at someone else — a different bug, session hijacking). This pile is mostly fine, if the values are long and random. - Meaningful values.
role=user,cart={"id":42},theme=dark,isPremium=false. These carry their own meaning. And this is the dangerous pile, because the meaning is sitting on the client, in plaintext or something you can trivially decode, waiting to be edited.
The whole technique is one line: find a meaningful value the server trusts, change it, and see if the server believes you.
Section 2
The move, step by step
Read · decode · change · re-encode · send
Five steps · The skill is recognising the load-bearing value
Read it, change it, send it back
- Read it. In DevTools, or a proxy like Burp / mitmproxy, capture the
Cookie:header. - Decode it. Meaningful values are rarely raw. Common wrappers, in order of how
often you see them:
- URL-encoding —
%7B%22id%22%3A42%7Dis just{"id":42}. - Base64 —
eyJyb2xlIjoidXNlciJ9decodes to{"role":"user"}. (A string ending in=or made ofA–Za–z0–9+/is your tell.) - JSON, nested inside either of the above.
- URL-encoding —
- Change it.
{"role":"user"}→{"role":"admin"}.isPremium=false→true.cart={"id":42,"price":9.99}→"price":0.01.user_id=1007→1008(that last one is a cookie-flavoured IDOR — the account you see is the number in the cookie). - Re-encode it the same way you found it — re-JSON, re-base64, re-URL-encode — so the shape matches what the server expects.
- Send it back and watch the response. If the app now thinks you are an admin, or the cart totals a penny, the server trusted a value the client controls. That is the finding.
The findingThe server made an authorization or pricing decision from a value the client can freely edit.
Tooling is deliberately boring: browser DevTools to edit a value in place, an extension like Cookie-Editor, or a proxy to intercept and rewrite on the fly. The skill is not the tool; it is recognising which value is load-bearing.
Section 3
Why it works
The trust boundary, drawn wrong
The architectural mistake · The user’s pocket is not a safe
State the server let the client hold
Every one of these bugs is the same architectural mistake: the server made a decision
based on state it let the client hold. role=admin in a cookie is the
server saying “I’ll just keep the answer to ‘is this person an
admin’ in the user’s pocket and read it back later.” The user’s
pocket is not a safe.
SameSite, Secure and HttpOnly do not save
you here. Those flags stop other sites and scripts from stealing the cookie;
they do nothing to stop the legitimate user from editing their own cookie in their own
browser. HttpOnly on role=admin protects the value from XSS and
still lets the account’s owner set it to admin by hand.
| Flag | What it actually protects against | Does NOT stop |
|---|---|---|
HttpOnly | a script (XSS) reading the cookie value | the owner editing their own cookie |
Secure | interception of the cookie over plaintext HTTP | the owner editing their own cookie |
SameSite | another site sending the cookie for you (CSRF) | the owner editing their own cookie |
Read the table down its last column: every flag defends against a third party. None
of them defends against the person holding the cookie, and the person holding the cookie is
exactly who edits role=admin.
Section 4
How to build it so this never happens
One decision kills the entire class
Recommendations · Keep meaning on the server
Build against it
- Keep meaning on the server. Put only a random handle in the cookie.
session=<128 bits of randomness>and look up the role, the cart, the price, the entitlements in your own store. The client holds a ticket stub, not the ticket’s contents. This single decision kills the entire class. - If a value MUST live client-side, sign it. A signed cookie or a JWT carries an
HMAC / signature the server verifies, so a tampered value is rejected. But signing has its
own footguns — the
alg:noneJWT bug (a token that says “I’m not signed, trust me”) is a whole family of failures, and a signed price is still a price you should have looked up server-side anyway. Sign integrity-critical values you truly cannot avoid shipping; do not use signing as an excuse to ship trust-bearing data to the client. - Never authorize off a client value. “Is this user an admin?” is answered by the server’s record of who they are, every request, not by a field they can type.
- Set
HttpOnly,Secure,SameSiteanyway — for the other threats (XSS theft, CSRF, transport). Just do not mistake them for a defense against the owner editing their own value.
Section 5
Why we care, at ECHO
The line between “I found a bug” and “I understand the system”
Takeaway · Ask it of every value the client sends back
A finding, and a defensive habit
This is on the short list of things that separate “I found a bug” from “I understand the system.” Cookie tampering shows up in nearly every intro web CTF (BrunnerCTF’s CakeSearch was a cousin of it — a forged role claim in a client-minted token), and it maps one-to-one onto a real defensive habit: when you build something, ask of every value the client sends back, “what happens if they changed this?” If the honest answer is “something bad,” the value belongs on the server.
A lab to practice on, safely, is a good follow-up — a tiny Flask app that trusts
role= in a cookie, so you can break it and then fix it. Ask in the club channel
if you want the exercise.
ECHO · Technique write-ups · Web fundamentals
Sanitized technique explainer by Digital Orukami · lab-safe, no live target · educational use only.