Sessions and Tokens — From the Browser to the Mesh
A cookie alone can't tell where a request came from
In one line
CSRF is an attack that borrows the property that the browser attaches cookies automatically and gets another site to send a state-changing request without the user knowing. Looking at the cookie alone, you cannot tell whether the request started from our page, so the server must separately require a signal that an attacking page cannot imitate.
Why this was needed
A user is logged in to bank.example.com and opens the attacker's page in another tab. That page has a hidden form that points to bank.example.com/transfer and is submitted as soon as it opens. The browser attaches the bank's cookie because the destination is the bank. The request that arrives at the server is not one character different from a transfer the user pressed themselves.
The attacker cannot read the response. The same-origin policy hides a cross-origin response. So CSRF is not an attack that steals but one that writes, and the defenses developed in the direction of checking "did this write really come from our page?"
How it works
The defenses summarized by the OWASP CSRF prevention cheat sheet fall into four lines.
Synchronizer token. The server makes an unpredictable value for each session, stores it in the session, and hands it down to the page. A state-changing request must send that value back in a header or a hidden field. The attacking page cannot read that value because of the same-origin policy. OWASP recommends not sending the token as a cookie and using a custom header rather than a form field. What is often left out here is binding to the session. If you check only "is this a token we issued", an attacker can put a genuine token received with their own account into the victim's request and get it through. You do the comparison with a constant-time function such as hmac.compare_digest so that timing does not leak.
Signed double-submit cookie. You use this when you do not want to keep state on the server. You bind a random value and the session ID, sign them with an HMAC, and carry the signed value in both the cookie and the header. OWASP does not recommend the approach that only checks whether the cookie and the header are the same, without a signature. This is because an attacker who can inject cookies from a subdomain or a network position can make the two values match exactly.
Fetch Metadata and Origin. The browser attaches Sec-Fetch-Site to every request. The values are same-origin, same-site, cross-site, and none, and because it is a forbidden header that starts with Sec-, page scripts cannot change it. OWASP says to reject a cross-site POST, PUT, PATCH, or DELETE, and to allow same-site only when you trust sibling subdomains. For an old client without the header, you either block it (recommended for sensitive endpoints) or pass it on to judge by an Origin check and a token. The Origin header is checked against an allowlist for equality of the whole string. If you compare only the beginning, a name such as http://127.0.0.1:8303.evil.example gets through.
Custom header. To attach a custom header to a cross-origin request, it has to pass a CORS preflight. If the server does not allow it, the request does not go out. So if you open CORS loosely, this defense collapses along with it.
SameSite is a supplement. In the RFC 6265bis draft, a Lax cookie is carried on same-site requests and, even cross-site, on top-level navigations with safe methods. There are three limits.
same-site ≠ same-origin blog.example.com 과 bank.example.com 은 같은 사이트다
GET 으로 상태 변경 링크 하나로 최상위 GET 이동이 되고 Lax 쿠키가 실린다
기본값의 예외 속성이 없으면 Lax 와 같은 기본 모드지만 브라우저가
Lax-allowing-unsafe 를 쓸 수 있다
About the last line, the draft says to make an exception only for recently created cookies in such browsers and cites 2 minutes or less as a reasonable limit. According to web.dev, from version 80 Chrome treats a cookie with no attribute as Lax but put in a temporary mitigation that allows the cookie on a top-level POST for about 2 minutes. An explicit SameSite=Lax does not have this exception. OWASP's conclusion is the same — SameSite is only defense in depth and does not replace a CSRF defense.
A login form is a target too. Login CSRF logs the victim in to the attacker's account so that what the victim enters piles up in the attacker's account. Before login there is no session, so there is nowhere to bind a token; OWASP says to create a pre-login session and load a token onto it, and to create a fresh session after login.
What it looks like in the field
If endpoints that change state with GET, such as GET /logout or GET /approve?id=42, remain, SameSite=Lax blocks nothing. A single link carries the cookie. This is why OWASP separately states "do not use GET for state-changing operations".
It is also common for a company's services to be scattered across several subdomains under one registrable domain. If XSS appears on one campaign page, that page sends a same-site request to the bank subdomain. SameSite lets it through, and only a Fetch Metadata policy that does not trust siblings and a token bound to the session stop it.
What you will do in the next lab
You stand up a transfer server that you can log in to at 127.0.0.1:8303 and issue a per-session CSRF token. The grader makes forged requests directly with curl. You send a request with no token, another session's token, Sec-Fetch-Site: cross-site, and an Origin outside the allowlist, and confirm that all are 403 and that the transfer was not recorded. Finally, you fill in a judgment table of which cross-site requests a SameSite=Lax cookie is carried on.