The attack

The victim is logged into bank.example. They visit a malicious page that contains:

<form action="https://bank.example/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit()</script>

The browser submits the form. The bank's session cookie comes along. The transfer happens.

The modern defaults

1. SameSite cookies

The first line of defense. Cookies are attached to cross-site requests only when their SameSite attribute permits it.

ValueBehavior
StrictNever sent on cross-site requests. Strongest, but breaks legitimate cross-site navigation.
Lax (default in modern browsers)Sent only on top-level navigations (clicking a link). Blocks form POSTs from other sites.
NoneAlways sent. Requires Secure; used when cross-site embedding is needed.

Modern browsers default to Lax when no attribute is set, which alone defeats the example above.

2. Anti-CSRF tokens

Embed a server-issued, per-session secret in every state-changing form. The browser cannot read it from a third-party context.

3. Custom request headers

Browsers won't let cross-origin requests send custom headers without a CORS preflight. Requiring a header like X-Requested-With: XMLHttpRequest on state-changing endpoints turns CSRF into a CORS problem the browser already protects against.

4. Origin / Referer validation

For sensitive endpoints, check the Origin or Referer header matches your domain. Belt-and-braces, especially behind reverse proxies.

i

GET requests should not change state. If they did, attackers could trigger them with <img> tags, bypassing many CSRF defenses. This is a long-standing HTTP convention — and a hard rule for safe API design.