Skip to content
Let’s plan the right software for your processes. Call us for a demo or a quote: +90 546 737 48 29

TR EN DE

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?

  1. 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.

    Diagram: triggered by another page, the browser of a logged-in user sends a request with the cookie to the site : Enlarge
  2. 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.

    Diagram: the form arrives with a token tied to the session and the server verifies it; a request without the token is rejected : Enlarge
  3. The layers complement one another

    The token is the main protection. The SameSite cookie attribute provides extra protection at browser level; checking the Origin header adds a third control; and not performing state-changing actions with GET is the precondition for these protections to work.

    Diagram: CSRF defence layers; token, SameSite cookie, Origin check, use of POST, re-authentication for critical actions : 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 SameSite attribute of the session cookie in the developer tools; if it is undefined or None, 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 Referer or Origin value 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
<?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:

PHP
<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
<?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 action

Points to watch:

  • Do not put the token in the URL; it can leak through the address bar, the logs and the Referer header.
  • 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.

JavaScript
// 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
<?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
<?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.
PHP
<!-- 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 $_REQUEST also 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=Lax or Strict, and also Secure and HttpOnly.
  • 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

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