Imagine you are logged into your website's dashboard and then open another tab containing a malicious page. That page does not need to know your password. If the website lacks adequate protection, the browser can automatically send the login cookie when that page attempts to perform actions like changing an email, deleting data, or altering account settings.
This is the essence of Cross-Site Request Forgery or CSRF: other sites exploit an active login status to send fake requests to a trusted website. The attack is not always visible. The victim may feel like they are just viewing a regular page, while the browser operates using the permissions it already has.
CSRF is Different from XSS
CSRF and XSS are often mentioned together, but they are different issues. In XSS, the attacker tries to execute malicious JavaScript within the target website's page. In CSRF, the attacker uses the victim's browser to send requests that appear legitimate to the target website.
CSRF typically becomes a risk when applications use cookies for authentication. Browsers are designed to automatically send cookies to the appropriate domain. This convenience can also be exploited if the server does not request additional proof that the request genuinely originates from the website itself.
The risk depends on the available actions. On simple websites, the impact may be limited to profile changes. In business applications, CSRF can relate to changes in payment accounts, creating new users, deleting content, or altering access rights.
Do Not Use GET to Change Data
The first step that is often overlooked is separating requests for reading data from those for changing data. A URL like /delete-user?id=25 is dangerous if simply visiting it results in immediate data deletion.
GET requests should be safe and not alter the application's state. Use POST, PUT, PATCH, or DELETE for actions that change data. However, merely changing GET to POST is not enough. A POST form can still be submitted by a page from another domain, so CSRF protection is still required.
Token Pattern in PHP Applications
A common approach is the synchronizer token. The server generates a random token associated with the user's session. This token is included in the form. When the form is submitted, the server compares the token from the request with the token stored in the session.
The token must be generated using a secure random source and should not be easily guessable. The following simple example illustrates the pattern:
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf_token'];
?>
<form method="post" action="/ubah-profil.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Save</button>
</form>On the processing side, do not trust the received values directly:
<?php
session_start();
$token = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';
if (!$sessionToken || !hash_equals($sessionToken, $token)) {
http_response_code(403);
exit('Invalid request.');
}
// Continue input validation and process data changes.
?>Important Note: CSRF tokens are not a substitute for authentication and authorization. After the token passes, the application must still check whether the user is allowed to change that data. Do not allow regular users to change someone else's profile just because their token is valid.
Using Nonce Correctly in WordPress
WordPress provides a nonce mechanism to help prevent CSRF in forms, URLs, and AJAX. For forms, use wp_nonce_field() when displaying the form, then validate with check_admin_referer() or wp_verify_nonce() when processing the request.
<form method="post">
<?php wp_nonce_field('save_settings', 'settings_nonce'); ?>
<input type="text" name="site_name">
<button type="submit">Save</button>
</form><?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
check_admin_referer('save_settings', 'settings_nonce');
if (!current_user_can('manage_options')) {
wp_die('You do not have permission.');
}
// Sanitize input, then save changes.
}
?>The order is important. Nonce checks whether the request has the expected token, while current_user_can() checks permissions. Both answer different questions. WordPress itself emphasizes that nonces should not be used as a substitute for authentication, authorization, or capability checks.
Additional Layers of Protection
Tokens should be the primary defense, not the only layer. The SameSite cookie attribute can help limit cookie transmission in cross-site contexts. For sensitive sessions, use appropriate cookie configurations like Secure and HttpOnly, and ensure the entire website runs over HTTPS.
The server can also check the Origin or Referer headers as an additional check. In modern applications, Fetch Metadata headers like Sec-Fetch-Site can also help detect cross-site requests. However, do not rely on a single header for security, as proxies, older browsers, or specific configurations may make headers unavailable.
If the application has very sensitive operations, add a confirmation step. For example, request the password again, an additional authentication code, or explicit confirmation before changing payment accounts and deleting accounts.
What You Can Do Now
- Inventory all endpoints that change data, including AJAX and REST API endpoints.
- Ensure that no data-changing actions are triggered by GET.
- Add CSRF tokens to all forms and requests that change the application's state.
- Validate tokens on the server before performing queries or data changes.
- Use capability or role checks separately from the token.
- Review WordPress plugins that create forms, delete buttons, or AJAX endpoints themselves.
- Log requests with invalid tokens to identify attack patterns and integration errors.
CSRF is not an issue that only arises in large applications. Simple settings forms, delete buttons, and custom AJAX endpoints are enough to create vulnerabilities. The good news is that protection does not have to be complicated: use the correct HTTP methods, implement hard-to-guess tokens, validate on the server, and remember to check user permissions.
Sources & Further Reading
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- WordPress Nonces β Common APIs Handbook
- WordPress Authentication β REST API Handbook
- PHP Session Management Basics
β Rio Yotto @rioyotto
