Imagine you are logged into a website dashboard and then open another tab to read an article. Unbeknownst to you, the second page attempts to send a request to the first website—such as changing your account email, altering settings, or deleting data. Because the browser still carries the login cookie, the server may assume the request genuinely comes from you.
This is a simple illustration of Cross-Site Request Forgery or CSRF. The issue is not always about leaked passwords. Often, applications are too trusting that every request from a logged-in user's browser must originate from that user's actions.
What actually happens in a CSRF attack?
CSRF exploits the browser's habit of automatically including cookies when sending requests to a website. An attacker does not need to know the contents of the cookie or take over an account. They only need to create a page or link that triggers a request to the target website.
For example, an old endpoint might change an order status simply with a URL like /order/123/approve. If that endpoint can be called with a GET method and does not require additional proof, another page can lure the victim's browser to access it.
Therefore, using the POST method alone does not automatically prevent CSRF. Hidden forms or JavaScript from other sites can still attempt to send POST requests under certain conditions. Protection must check whether the request was indeed made by a legitimate application page.
CSRF Token: a small signature for every important action
The most common approach is the synchronizer token. The server generates a random token or a token tied to the user's session, then embeds it into the form. When the form is submitted, the server checks the token. Other sites may trigger requests, but they typically cannot read the token that is only available on the target application page.
A simple example in PHP:
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="/profile/update.php">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Save</button>
</form>On the server side, the token must be compared with the value stored in the session before any changes are made:
$submitted = $_POST['csrf_token'] ?? '';
$expected = $_SESSION['csrf_token'] ?? '';
if (!$expected || !hash_equals($expected, $submitted)) {
http_response_code(403);
exit('Invalid request');
}The random_bytes() function is designed to generate cryptographically secure random bytes. Meanwhile, hash_equals() helps compare secret values without leaking information through execution time differences. Both are available in modern PHP and documented in the official PHP manual.
Do not make tokens a substitute for authorization
CSRF tokens only answer the question: “Is this request likely made from a legitimate application page?” Tokens do not answer: “Is this user authorized to delete that data?”
Each endpoint still needs to check authentication and authorization. For example, a user who can edit their profile may not necessarily be allowed to delete another account. The order should be clear:
- Ensure the user is logged in.
- Check the CSRF token.
- Validate the input sent.
- Check access rights to the object or action.
- Then make the data changes.
WordPress documentation also emphasizes that nonce is not an authentication or access control mechanism. Nonce helps reduce CSRF risk but must still be used alongside user capability checks like current_user_can().
How is it implemented in WordPress?
If you are creating a WordPress plugin or admin page, use the built-in nonce API instead of creating your own token system without a strong reason. For forms, WordPress provides wp_nonce_field() when creating tokens and check_admin_referer() or wp_verify_nonce() when checking them.
// When displaying the form
wp_nonce_field('update_profile', 'profile_nonce');
// When processing the form
if (
!isset($_POST['profile_nonce']) ||
!wp_verify_nonce(
sanitize_text_field(wp_unslash($_POST['profile_nonce'])),
'update_profile'
)
) {
wp_die('Invalid request.');
}
if (!current_user_can('edit_users')) {
wp_die('You do not have permission.');
}For the WordPress REST API that uses cookie-based authentication, a nonce with the action wp_rest is used to help validate requests. However, the endpoint still needs to check permission_callback to ensure that only users with the appropriate rights can perform the action.
SameSite helps, but it’s not the only layer
The SameSite cookie attribute makes browsers more selective in sending cookies on cross-site requests. The Lax value often serves as a compromise between security and compatibility, while Strict is more stringent. However, SameSite should be viewed as defense in depth, not a substitute for CSRF tokens for all applications.
There are several reasons. Websites may have subdomains that are not all trusted. Additionally, endpoints that change data via GET are still at risk when cookies with SameSite=Lax are sent during certain navigations. A safer principle is to use GET only for reading data, while changes should use POST, PUT, PATCH, or DELETE and remain protected.
CSRF Check Checklist
- Ensure all actions that change data are not triggered via GET.
- Add CSRF tokens to forms and AJAX requests that require cookie authentication.
- Do not place CSRF tokens in URLs as they can enter browser history, logs, or the Referer header.
- Use HTTPS and set session cookies with
Secure,HttpOnly, andSameSiteas needed. - Check the token before business validation and before database changes.
- Do not consider CSRF tokens as a substitute for role or capability checks.
- Test sensitive endpoints with a logged-in browser and pages from different origins.
What does this mean for us?
CSRF typically does not appear as a dramatic attack. There are no fake login screens, and there are not always clear errors. The impact is only felt when settings change, data is deleted, or important actions are taken on behalf of the user.
The fix is relatively simple: use securely generated tokens, validate on the server, limit changes to the appropriate HTTP methods, and continue to check user permissions. For WordPress, leverage the built-in security API. For PHP, use appropriate randomness sources and secret comparisons, then make CSRF protection part of the standard pattern whenever creating new endpoints.
Technical references: OWASP CSRF Prevention Cheat Sheet, WordPress Nonces, and PHP random_bytes().
Sources & Further Reading
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- WordPress Nonces – Common APIs Handbook
- WordPress REST API Authentication
- PHP random_bytes() Manual
- PHP hash_equals() Manual
– Rio Yotto @rioyotto
