Home / Articles / Web Security
Web Security

POST Forms Are Not Safe Yet: How to Prevent CSRF in PHP and WordPress

Websites can receive malicious requests from a logged-in user's browser, even if the user never clicks any buttons. Understand CSRF and use tokens, permission checks, and cookie configurations to…

Form POST Saja Belum Aman: Cara Mencegah CSRF di PHP dan WordPress

Websites that receive data from users typically use POST to save changes. However, simply changing GET to POST does not automatically make it secure. If a user is logged in, the browser will automatically send session cookies—even when requests are triggered from another site.

This is where Cross-Site Request Forgery or CSRF can occur. Simply put, the user does not consciously give a command, but their browser helps send a request that appears legitimate to the website they are logged into. The impact can include changes to email, password changes, data deletion, or unwanted transactions. OWASP explains that this attack exploits how browsers automatically send authentication cookies. ([cheatsheetseries.owasp.org](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html?utm_source=openai))

Simple Example of a CSRF Attack

Imagine there is an endpoint like /ubah-email.php that only checks if the user is logged in:

<form action="https://contoh.com/ubah-email.php" method="POST">
  <input type="hidden" name="email" value="attacker@example.com">
</form>

This form can be placed on another site and automatically submitted when the page is opened. If the victim still has an active session on contoh.com, the browser may also send the session cookie. The server then assumes the request genuinely comes from the user.

Such attacks do not require password theft. The attacker simply tries to make the victim's browser send a request to a trusted website.

Do Not Rely on POST, HTTPS, or Referer Alone

Some security assumptions may sound reasonable, but they are not sufficient as primary protection.

  • POST does not prevent CSRF. HTML forms from other sites can still send POST requests.
  • HTTPS does not prevent CSRF. HTTPS protects the connection but does not prove that the user intended to perform the action.
  • Referer checks alone are weak. This header may not be sent due to privacy policies, proxies, or certain browser configurations.
  • Requesting confirmation at a second step may not be enough. If all steps can be guessed and lack additional protection, attacks may still be possible.

HTTPS should still be used, but its role is as a security foundation, not a replacement for CSRF tokens. OWASP also recommends that operations that change data should not be performed via GET. ([community.owasp.org](https://community.owasp.org/attacks/csrf?utm_source=openai))

Main Pattern: CSRF Token on the Server

A common approach for session-based PHP applications is the synchronizer token. The server generates a random token, stores it in the user's session, and then embeds it in the form. When the form is submitted, the server compares the token from the form with the token in the session.

The token must be generated using a secure random source and should not be guessable. In PHP, a simple example can use random_bytes():

<?php
session_start();

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$csrfToken = $_SESSION['csrf_token'];
?>

<form method="post" action="simpan-profil.php">
  <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
  <button type="submit">Save</button>
</form>

On the receiving endpoint, do not just check if the field exists. Compare its value with the token in the session using hash_equals() to ensure the comparison is not easily readable through execution time differences:

<?php
session_start();

$token = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';

if (!$sessionToken || !$token || !hash_equals($sessionToken, $token)) {
    http_response_code(403);
    exit('Invalid request.');
}

// Continue with input validation and storage processing.

The token is not a substitute for authentication and authorization. After the token is validated, the application must still check whether the user has the right to change that data.

What About WordPress?

WordPress provides a feature called nonce to help protect forms and URLs from misuse, including CSRF. However, the term “nonce” in WordPress does not mean a token that can truly only be used once. The token has a specific expiration and context.

An example of creating a nonce field in a plugin form:

<?php wp_nonce_field('simpan_pengaturan', 'pengaturan_nonce'); ?>

In the handler, the token needs to be verified:

<?php
if (
    !isset($_POST['pengaturan_nonce']) ||
    !wp_verify_nonce($_POST['pengaturan_nonce'], 'simpan_pengaturan')
) {
    wp_die('Invalid request.');
}

if (!current_user_can('manage_options')) {
    wp_die('You do not have permission.');
}

The important part is that wp_verify_nonce() and current_user_can() serve different functions. Nonce helps ensure the context of the request, while capability checks determine whether the user is allowed to perform the action. The WordPress documentation explicitly warns against using nonce as a substitute for authentication or access control. ([developer.wordpress.org](https://developer.wordpress.org/apis/security/nonces/?utm_source=openai))

Add Protection to Session Cookies

CSRF tokens remain the primary defense, but cookie attributes can help reduce risk. Session cookies should use Secure to ensure they are only sent over HTTPS, HttpOnly to prevent JavaScript from reading them, and SameSite=Lax or Strict as needed based on the application flow.

<?php
session_set_cookie_params([
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax'
]);
session_start();

PHP supports setting Secure, HttpOnly, and SameSite through session_set_cookie_params(). However, SameSite should be considered an additional layer, not a reason to eliminate CSRF tokens. The Lax behavior, for example, still allows cookies on some cross-site navigations. ([php.net](https://www.php.net/manual/en/function.session-set-cookie-params.php?utm_source=openai))

What Does This Mean for Us?

CSRF often arises not because the code looks complicated, but because endpoints are too trusting of session cookies. Every action that changes state—editing profiles, changing passwords, deleting content, placing orders, or changing settings—needs to be treated as a sensitive operation.

What You Can Do Now

  1. List all endpoints that change data and ensure they do not use GET.
  2. Add CSRF tokens to every form or AJAX request that uses cookie-based authentication.
  3. Validate tokens on the server, not just through JavaScript.
  4. Use capability or role checks after the token is validated.
  5. Enable Secure, HttpOnly, and SameSite cookie attributes according to your application architecture.
  6. Reject missing or mismatched tokens with a 403 response, and log the event without storing the token in logs.
  7. Test critical flows from different domains to ensure requests without tokens are truly rejected.

CSRF protection does not have to start from a large system. One random token, one server-side check, and one permission check are already much better than relying solely on POST methods or the padlock icon in the browser.

Sources & Further Reading

Explore Also

– Rio Yotto @rioyotto