What is CSRF and how do you prevent it?
CSRF (cross-site request forgery) is when the browser of a logged-in user is made to send a request to your site without the user knowing. The browser automatically attaches a site's cookies to every request going to that site. If the user visits another page while logged in to your site, that page can make the browser send a request to your site, and the request is processed with the user's session.
The result is that every action the user is able to perform can be performed in their name: changing the email address, deleting a record, adding a new user in the admin panel. The attacker cannot see the response; the aim is not to read data but to get an action carried out. The solution is to verify that the request really came from your own page.
In brief
- CSRF is when the browser of a logged-in user is made to send a request without the user knowing.
- The main protection is a CSRF token in every form, verified on the server.
- SameSite cookies and Origin checks are additional layers.
- State-changing actions are never performed with GET.
On this page
How does CSRF work and where is it stopped?
-
The browser sends the cookie automatically
The root of the problem is that the server accepts the request by looking only at the session cookie. Because the cookie goes along with every request automatically, the server cannot tell whether the user made the request of their own accord or whether another site triggered it.
: Enlarge -
The token proves the request came from your page
A CSRF token is a random, unpredictable value generated by the server and tied to the session. It is added to your forms as a hidden field and verified on the server when the request arrives. Because another site cannot know this value, the request it triggers is rejected.
: Enlarge -
The layers complement one another
The token is the main protection. The
SameSitecookie attribute provides extra protection at browser level; checking theOriginheader adds a third control; and not performing state-changing actions with GET is the precondition for these protections to work.
: Enlarge
Signs: how do you spot it?
- In a code review: If forms that change data have no hidden token field, or the server does not verify that field, there is no protection. If actions such as deleting, approving or changing a status are performed with a link (GET), the vulnerability is certain.
- In the browser: Look at the
SameSiteattribute of the session cookie in the developer tools; if it is undefined orNone, there is no extra protection. - On the user side: Settings changes that the user says they did not make, records added or deleted in their name.
- In the logs: State-changing requests where the
RefererorOriginvalue is an address unrelated to your site.
Implementing a CSRF token
The token is generated once per session, stored in the session and sent as a hidden field in every form. The comparison is done with hash_equals; this function prevents information leaking through timing differences.
<?php
session_start();
function csrfBelirteci(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function csrfDogrula(?string $gelen): bool
{
return is_string($gelen)
&& !empty($_SESSION['csrf'])
&& hash_equals($_SESSION['csrf'], $gelen);
}Using it in a form:
<form method="post" action="profil-guncelle">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrfBelirteci(), ENT_QUOTES, 'UTF-8') ?>">
<label for="eposta">Email</label>
<input type="email" id="eposta" name="eposta" required>
<button type="submit">Save</button>
</form>When processing the request, verify it before doing anything else:
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
echo 'This action can only be performed by submitting the form.';
return;
}
if (!csrfDogrula($_POST['csrf'] ?? null)) {
http_response_code(403);
echo 'Your session could not be verified. Refresh the page and try again.';
return;
}
// ... verification passed: perform the actionPoints to watch:
- Do not put the token in the URL; it can leak through the address bar, the logs and the
Refererheader. - When the user logs in, renew the token together with the session ID.
- Protect the login form itself as well; otherwise the user can be logged in to someone else's account without realising it.
- Frameworks such as Laravel and Symfony and most content management systems offer built-in CSRF protection; use it instead of writing your own, and do not disable it.
AJAX and API requests
For requests sent with JavaScript, the token is usually read from a meta tag and sent in a custom request header. The server verifies the value in the header in the same way.
// In the page: <meta name="csrf" content="...token generated by the server...">
var belirtec = document.querySelector('meta[name="csrf"]').content;
fetch('sepet/ekle', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': belirtec },
credentials: 'same-origin',
body: JSON.stringify({ urun: 12, adet: 1 })
});
// On the server: csrfDogrula($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null)In APIs that authenticate not with a cookie but with a token sent in the Authorization header on every request, the CSRF risk is different, because the browser does not attach the credential automatically. Every endpoint that uses cookies, however, must be protected.
The SameSite cookie attribute
SameSite determines whether the cookie is sent with requests initiated from other sites:
- Strict: The cookie is sent only with requests initiated from the same site. This is the most secure option; however, when the user arrives from another site (for example from a link in an email), they do not appear logged in on the first request.
- Lax: The cookie is sent on top-level navigations coming from another site (clicking a link); it is not sent with POST requests made from another site. This is the right balance for most sites.
- None: The cookie is sent with every request; it must be used together with
Secure. It is only for cookies that genuinely need to work in a third-party context.
<?php
// Session cookie (before session_start)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
// Your own cookies
setcookie('tercih', 'koyu', [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);Many current browsers treat cookies with no attribute specified as Lax; do not rely on this, though, and state the value explicitly. SameSite is not considered enough on its own: Lax does not protect state changes made with GET, and if one of your subdomains is compromised it counts as the "same site". Use it together with a token.
Origin and Referer checks
Browsers add the Origin header to POST requests initiated from another origin. On state-changing requests, checking that this header matches your own domain is an additional control on top of the token. If the header is missing, the Referer value can be checked; if neither is present, leave the request to token verification.
<?php
function ayniKaynak(): bool
{
$kaynak = $_SERVER['HTTP_ORIGIN'] ?? ($_SERVER['HTTP_REFERER'] ?? '');
if ($kaynak === '') {
return true; // no header: the decision is left to token verification
}
$gelen = strtolower((string) parse_url($kaynak, PHP_URL_HOST));
$izinli = ['www.example.com', 'example.com']; // your own domains
return in_array($gelen, $izinli, true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !ayniKaynak()) {
http_response_code(403);
echo 'The request was not accepted.';
return;
}Do not use GET for state-changing actions
GET requests should only read data. If actions such as deleting, approving, placing an order or changing a setting are performed with a link of the form ?action=delete&id=5, any page containing that address can trigger the action; in addition, search engine bots and the browser's preloading feature may follow these links.
- Turn actions of this kind into a small POST form; if the look of a link is wanted, the button can be styled with CSS.
- Check the request method on the server; reject GET where POST is expected.
- Logging out should also be done with POST.
<!-- WRONG: <a href="kayit-sil?id=5">Delete</a> -->
<form method="post" action="kayit-sil">
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrfBelirteci(), ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="id" value="<?= (int) $kayit['id'] ?>">
<button type="submit">Delete</button>
</form>Re-authentication for critical actions
For critical actions such as changing the password or email address, updating payment details or adding an administrator, ask the user for their current password or two-factor authentication code again. This measure is effective both against CSRF and against the misuse of sessions that have been left open.
Remember: if your site has an XSS vulnerability, the attacker's script can read the token on the page and get past the CSRF protection. Think of the two protections together.
Common mistakes
- Generating the token but not verifying it: The form has the hidden field, but the server does not compare the value. The protection lies in the verification line.
- Accepting an empty value: When there is no token in the session, an incoming empty value comes out "equal" to the empty value. In the verification, check that both sides are non-empty.
- Protecting only some forms: Small actions in the admin panel such as deleting, reordering and changing a status get forgotten. Protection should be applied to all POST requests at one common point, not form by form.
- Keeping the token in a cookie and checking only the cookie: The cookie goes with every request automatically anyway; the value to be compared must come from the form or from a request header.
- Processing a GET request as if it were a POST: Code that uses
$_REQUESTalso accepts parameters arriving by GET. Check the request method explicitly and read the data from$_POST. - Forgetting JSON endpoints: The assumption "this address is only called from JavaScript" is not protection; every endpoint that authenticates with a cookie must require a token.
- Carrying on with the action after a failure: If verification fails, the request must end there; showing a warning and performing the action anyway makes the protection meaningless.
Checklist
- Every form that changes data has a CSRF token, and it is verified on the server with
hash_equals. - AJAX requests send the token in a header.
- Login and logout are protected too.
- No state-changing action is performed with GET.
- The session cookie is
SameSite=LaxorStrict, and alsoSecureandHttpOnly. - The Origin check is applied as an additional layer.
- For critical actions the password or second factor is requested again.
- The framework's built-in CSRF protection is on; addresses defined as exceptions have been reviewed.
Frequently asked questions
I use SameSite=Lax; do I still need a token?
Yes. SameSite is a strong additional layer, but it does not protect changes made with GET, may be missing in old browsers, and your subdomains count as the "same site". Use it together with a token.
What is the difference between CSRF and XSS?
In XSS, the attacker's script runs in your page. In CSRF no script runs in your page; the user's browser is steered into sending a request to your site from another site. An XSS vulnerability can also get past CSRF protection.
Should I generate a separate token for each form?
A single token per session is enough for most sites and does not cause problems with the back button or multiple tabs. A token renewed on every request is stricter but makes usability harder.
Does a captcha prevent CSRF?
It makes it harder indirectly, but that is not its purpose and it cannot be used on every form. The right tool for CSRF is the token.
BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.
Related guides
- What is XSS and how do you prevent it?Context-aware output escaping, Content-Security-Policy, HttpOnly cookies and rich-text sanitising.
- Login and session securityPassword hashing, attempt limits, session fixation, cookie flags and authorisation checks on every request.
- Security headers and HTTPSHSTS, CSP, X-Content-Type-Options, Referrer-Policy and Permissions-Policy, with an .htaccess example.
- Website security guide: where should you start?Threat types, layered defence, priorities and a roadmap to all the security guides.
Let us review your website together
BYK Yazılım builds corporate websites. Write to us with any questions about your site.
Contact us Our corporate website service