Cross-site Request Forgery (Anti-CSRF) Protection in PHP

by Vincy. Last modified on August 6th, 2021.

Cross-Site Request Forgery (CSRF) attack is a common security abuse that happens around the world wide web. Protecting the server against this attack is a first-level protection mechanism in protecting your website.

Malicious users over the internet used to clone requests to attack vulnerable servers. This cloning may happen by embedding the malicious site link to the user’s web page.

Anti-CSRF implementation reduces the vulnerability of the website. With this protection, the website rejects malicious access that sends requests with no or wrong CSRF token.

The following diagram shows the user request validation against the CSRF attack. If a genuine user posts the form with the proper token, the server processes the request. It rejects otherwise, in the absence of the CSRF token parameter.

Form Handling with Anti CSRF Protection

We will see an example code PHP contact form with CSRF protection in this tutorial. With this protection, it ensures the genuineness of the request before processing it.

I have created a service in PHP to handle the security validation against the CSRF attack. The server will reject users’ requests that are without a token or invalid token.

If you want to have a contact form with CSRF protection and more security features, get Iris.

What is inside?

  1. About this example
  2. Generate CSRF token and create PHP session
  3. Render contact form with CSRF token
  4. Anti Cross-Site Request Forgery (CSRF) validation in PHP
  5. Security service to generate, insert, validate CSRF token
  6. Output: CSRF validation response from server

About this example

This code implements Anti CSRF protection in a PHP contact form. It renders a contact form. This form post handlers validate the user requests against CSRF attack.

On loading the landing page, the PHP script generates the CSRF token. The form footer will have this token as a hidden field. Also, it manages the token in a PHP session.

On posting the form fields, the PHP code will check for the CSRF token parameter. If found, then it validates it will the token from the session.

If the user sends a request without a CSRF token, then the server will reject the request. Also, if the token is not matched with the token from the session, then the server will reject the request.

On successful CSRF token validation, it will send the contact email to the target address. The following diagram shows the file structure of this example.

Anti CSRF Token Code File Structure

Generate CSRF token and create PHP session

On a landing page, the form footer script invokes SecurityService. This is a PHP class to generate a CSRF token.

It writes the token into a PHP session for future reference. It will help at the time of processing the CSRF validation after the form post.

The form footer is a framework file that loads the generated token into a hidden field.

The below code snippet is from the SecurityService.php to generate CSRF toke. The complete code of the service class is shown in the upcoming section of this article.

SecurityService.php (code to generate CSRF token)

    /**
     * Generate, store, and return the CSRF token
     *
     * @return string[]
     */
    public function getCSRFToken()
    {
        if (empty($this->session[$this->sessionTokenLabel])) {
            $this->session[$this->sessionTokenLabel] = bin2hex(openssl_random_pseudo_bytes(32));
        }

        if ($this->hmac_ip !== false) {
            $token = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $token = $this->session[$this->sessionTokenLabel];
        }
        return $token;
    }

Render contact form with CSRF token

This is a contact form HTML with the usual fields name, email, subject and message. Added to that it has a hidden field csrf-token with the generated token.

The submit action processes jQuery form validation before posting the parameters to the PHP.

The client-side validation script handles the basic validation on submit. It applies the not-empty check on each field.

index.php (HTML Template)

<html>
<head>
<title>CSRF Protection using PHP</title>
<link rel="stylesheet" type="text/css"
	href="assets/css/phppot-style.css" />
<script src="vendor/jquery/jquery-3.2.1.min.js"></script>
<style>
.error-field {
	border: 1px solid #d96557;
}

.send-button {
	cursor: pointer;
	background: #3cb73c;
	border: #36a536 1px solid;
	color: #FFF;
	font-size: 1em;
	width: 100px;
}
</style>
</head>
<body>
	<div class="phppot-container">
		<h1>CSRF Protection using PHP</h1>
		<form name="frmContact" id="cnt-frm" class="phppot-form"
			frmContact"" method="post" action="" enctype="multipart/form-data"
			onsubmit="return validateContactForm()">

			<div class="phppot-row">
				<div class="label">
					Name <span id="userName-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="userName"
					id="userName"
					value="<?php if(!empty($_POST['userName'])&& $type == 'error'){ echo $_POST['userName'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Email <span id="userEmail-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="userEmail"
					id="userEmail"
					value="<?php if(!empty($_POST['userEmail'])&& $type == 'error'){ echo $_POST['userEmail'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Subject <span id="subject-info" class="validation-message"></span>
				</div>
				<input type="text" class="phppot-input" name="subject" id="subject"
					value="<?php if(!empty($_POST['subject'])&& $type == 'error'){ echo $_POST['subject'];}?>" />
			</div>
			<div class="phppot-row">
				<div class="label">
					Message <span id="userMessage-info" class="validation-message"></span>
				</div>
				<textarea name="content" id="content" class="phppot-input" cols="60"
					rows="6"><?php if(!empty($_POST['content'])&& $type == 'error'){ echo $_POST['content'];}?></textarea>
			</div>
			<div class="phppot-row">
				<input type="submit" name="send" class="send-button" value="Send" />
			</div>
			
			<?php require_once __DIR__ . '/view/framework/form-footer.php';?>
			
		</form>
		<?php if(!empty($message)) { ?>
		<div id="phppot-message" class="<?php  echo $type; ?>"><?php if(isset($message)){ ?>
				    <?php echo $message; }}?>
                    </div>
	</div>
	<script src="assets/js/validate.js"></script>
</body>
</html>

This is the form footer script that triggers the service handler to generate tokens. The insertHiddenToken() writes HTML code to load the csrf token field into the form.

view/framework/form-footer.php

<?php
require_once __DIR__ . '/../../lib/SecurityService.php';
$antiCSRF = new \Phppot\SecurityService\securityService();
$antiCSRF->insertHiddenToken();

assets/js/validation.js

function validateContactForm() {
	var valid = true;
	$("#userName").removeClass("error-field");
	$("#userEmail").removeClass("error-field");
	$("#subject").removeClass("error-field");
	$("#content").removeClass("error-field");

	$("#userName-info").html("").hide();
	$("#userEmail-info").html("").hide();
	$("#subject-info").html("").hide();
	$("#content-info").html("").hide();

	$(".validation-message").html("");
	$(".phppot-input").css('border', '#e0dfdf 1px solid');

	var userName = $("#userName").val();
	var userEmail = $("#userEmail").val();
	var subject = $("#subject").val();
	var content = $("#content").val();

	if (userName.trim() == "") {
		$("#userName-info").html("required.").css("color", "#ee0000").show();
		$("#userName").css('border', '#e66262 1px solid');
		$("#userName").addClass("error-field");

		valid = false;
	}
	if (userEmail.trim() == "") {
		$("#userEmail-info").html("required.").css("color", "#ee0000").show();
		$("#userEmail").css('border', '#e66262 1px solid');
		$("#userEmail").addClass("error-field");

		valid = false;
	}
	if (!userEmail.match(/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/)) {
		$("#userEmail-info").html("invalid email address.").css("color",
				"#ee0000").show();

		$("#userEmail").css('border', '#e66262 1px solid');
		$("#userEmail").addClass("error-field");

		valid = false;
	}

	if (subject == "") {
		$("#subject-info").html("required.").css("color", "#ee0000").show();
		$("#subject").css('border', '#e66262 1px solid');
		$("#subject").addClass("error-field");

		valid = false;
	}
	if (content == "") {
		$("#userMessage-info").html("required.").css("color", "#ee0000").show();
		$("#content").css('border', '#e66262 1px solid');
		$("#content").addClass("error-field");

		valid = false;
	}

	if (valid == false) {
		$('.error-field').first().focus();
		valid = false;
	}
	return valid;
}

Anti Cross-Site Request Forgery (CSRF) validation in PHP

On submitting the token-embedded contact form, the form action executes the following script.

The SecuritySercive’s validate() function compares the posted token with the one stored in the session.

If a match found, then it will proceed further to send the contact email. Otherwise, it will acknowledge the user with an error message.

index.php (PHP CSRF validation and form handling)

<?php
use Phppot\MailService;

session_start();
if (! empty($_POST['send'])) {
    require_once __DIR__ . '/lib/SecurityService.php';
    $antiCSRF = new \Phppot\SecurityService\securityService();
    $csrfResponse = $antiCSRF->validate();
    if (! empty($csrfResponse)) {
        require_once __DIR__ . '/lib/MailService.php'; 
        $mailService = new MailService();
        $response = $mailService->sendContactMail($_POST);
        if (! empty($response)) {
            $message = "Hi, we have received your message. Thank you.";
            $type = "success";
        } else {
            $message = "Unable to send email.";
            $type = "error";
        }
    } else {
        $message = "Security alert: Unable to process your request.";
        $type = "error";
    }
}

?>

Security service to generate, insert, validate CSRF token

This service class created in PHP includes methods to process the CSRF protection-related operations.

It defines a class property to set the form token field name, session index.

It has methods to generates tokens and write them into the HTML and a PHP session.

It uses XSS mitigations while writing the form footer with the token.

Also, it has the option to exclude some URLs from the validation process. The excluded URLs bypass the CSRF validation process.

The code gets the current request URL from the PHP SERVER variables. Then, it compares it with the array of excluded URLs to skip the validation.

lib/SecurityService.php

<?php
/**
 * Copyright (C) Phppot
 *
 * Distributed under 'The MIT License (MIT)'
 * In essense, you can do commercial use, modify, distribute and private use.
 * Though not mandatory, you are requested to attribute Phppot URL in your code or website.
 */
namespace Phppot\SecurityService;

/**
 * Library class used for CSRF protection.
 * CSRF is abbreviation for Cross Site Request Forgery.
 * Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a
 * web application in which they're currently authenticated. Defn. by OWASP.
 *
 * User session based token is generated and hashed with their IP address.
 * There are types of operations using which the DDL are executed.
 * Submits using general HTML form and submits using AJAX.
 * We are inserting a CSRF token inside the form and it is validated against the token present in the session.
 * This ensures that the CSRF attacks are prevented.
 *
 * If you are customizing the application and creating a new form,
 * you should ensure that the CSRF prevention is in place. form-footer.php
 * is the file that should be included where the token is to be echoed.
 * After echo the validation of the token happens in controller and it is
 * the common entry point for all calls. So there is no need to do any separate code for
 * CSRF validation with respect to each functionality.
 *
 * The CSRF token is written as a hidden input type inside the html form tag with a label $formTokenLabel.
 *
 * @author Vincy
 * @version 3.5 - IP Address tracking removed as it is good for GDPR compliance.
 *         
 */
class securityService
{

    private $formTokenLabel = 'eg-csrf-token-label';

    private $sessionTokenLabel = 'EG_CSRF_TOKEN_SESS_IDX';

    private $post = [];

    private $session = [];

    private $server = [];

    private $excludeUrl = [];

    private $hashAlgo = 'sha256';

    private $hmac_ip = true;

    private $hmacData = 'ABCeNBHVe3kmAqvU2s7yyuJSF2gpxKLC';

    /**
     * NULL is not a valid array type
     *
     * @param array $post
     * @param array $session
     * @param array $server
     * @throws \Error
     */
    public function __construct($excludeUrl = null, &$post = null, &$session = null, &$server = null)
    {
        if (! \is_null($excludeUrl)) {
            $this->excludeUrl = $excludeUrl;
        }
        if (! \is_null($post)) {
            $this->post = & $post;
        } else {
            $this->post = & $_POST;
        }

        if (! \is_null($server)) {
            $this->server = & $server;
        } else {
            $this->server = & $_SERVER;
        }

        if (! \is_null($session)) {
            $this->session = & $session;
        } elseif (! \is_null($_SESSION) && isset($_SESSION)) {
            $this->session = & $_SESSION;
        } else {
            throw new \Error('No session available for persistence');
        }
    }

    /**
     * Insert a CSRF token to a form
     *
     * @param string $lockTo
     *            This CSRF token is only valid for this HTTP request endpoint
     * @param bool $echo
     *            if true, echo instead of returning
     * @return string
     */
    public function insertHiddenToken()
    {
        $csrfToken = $this->getCSRFToken();

        echo "<!--\n--><input type=\"hidden\"" . " name=\"" . $this->xssafe($this->formTokenLabel) . "\"" . " value=\"" . $this->xssafe($csrfToken) . "\"" . " />";
    }

    // xss mitigation functions
    public function xssafe($data, $encoding = 'UTF-8')
    {
        return htmlspecialchars($data, ENT_QUOTES | ENT_HTML401, $encoding);
    }

    /**
     * Generate, store, and return the CSRF token
     *
     * @return string[]
     */
    public function getCSRFToken()
    {
        if (empty($this->session[$this->sessionTokenLabel])) {
            $this->session[$this->sessionTokenLabel] = bin2hex(openssl_random_pseudo_bytes(32));
        }

        if ($this->hmac_ip !== false) {
            $token = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $token = $this->session[$this->sessionTokenLabel];
        }
        return $token;
    }

    /**
     * hashing with IP Address removed for GDPR compliance easiness
     * and hmacdata is used.
     *
     * @param string $token
     * @return string hashed data
     */
    private function hMacWithIp($token)
    {
        $hashHmac = \hash_hmac($this->hashAlgo, $this->hmacData, $token);
        return $hashHmac;
    }

    /**
     * returns the current request URL
     *
     * @return string
     */
    private function getCurrentRequestUrl()
    {
        $protocol = "http";
        if (isset($this->server['HTTPS'])) {
            $protocol = "https";
        }
        $currentUrl = $protocol . "://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'];
        return $currentUrl;
    }

    /**
     * core function that validates for the CSRF attempt.
     *
     * @throws \Exception
     */
    public function validate()
    {
        $currentUrl = $this->getCurrentRequestUrl();
        if (! in_array($currentUrl, $this->excludeUrl)) {
            if (! empty($this->post)) {
                $isAntiCSRF = $this->validateRequest();
                if (! $isAntiCSRF) {
                    // CSRF attack attempt
                    // CSRF attempt is detected. Need not reveal that information
                    // to the attacker, so just failing without info.
                    // Error code 1837 stands for CSRF attempt and this is for
                    // our identification purposes.
                    return false;
                }
                return true;
            }
        }
    }

    /**
     * the actual validation of CSRF happens here and returns boolean
     *
     * @return boolean
     */
    public function isValidRequest()
    {
        $isValid = false;
        $currentUrl = $this->getCurrentRequestUrl();
        if (! in_array($currentUrl, $this->excludeUrl)) {
            if (! empty($this->post)) {
                $isValid = $this->validateRequest();
            }
        }
        return $isValid;
    }

    /**
     * Validate a request based on session
     *
     * @return bool
     */
    public function validateRequest()
    {
        if (! isset($this->session[$this->sessionTokenLabel])) {
            // CSRF Token not found
            return false;
        }

        if (! empty($this->post[$this->formTokenLabel])) {
            // Let's pull the POST data
            $token = $this->post[$this->formTokenLabel];
        } else {
            return false;
        }

        if (! \is_string($token)) {
            return false;
        }

        // Grab the stored token
        if ($this->hmac_ip !== false) {
            $expected = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
        } else {
            $expected = $this->session[$this->sessionTokenLabel];
        }

        return \hash_equals($token, $expected);
    }

    /**
     * removes the token from the session
     */
    public function unsetToken()
    {
        if (! empty($this->session[$this->sessionTokenLabel])) {
            unset($this->session[$this->sessionTokenLabel]);
        }
    }
}

This MailService.php uses the PHP core mail() function to send the contact emails. You may replace this with the SMTP via email sending script.

Check this to get IP address using PHP. It may be useful to log the user’s IP address.

lib/MailService.php

<?php
namespace Phppot;

class MailService
{

    function sendContactMail($postValues)
    {
        $name = $postValues["userName"];
        $email = $postValues["userEmail"];
        $subject = $postValues["subject"];
        $content = $postValues["content"];

        $toEmail = "ADMIN EMAIL";
        $mailHeaders = "From: " . $name . "(" . $email . ")\r\n";
        $response = mail($toEmail, $subject, $content, $mailHeaders);

        return $response;
    }
}

Output: CSRF validation response from server

The screenshot shows the usual contact form below. We have seen this output in many of the contact form tutorials before.

Below the form interface, this screenshot shows the security alert message in red. It acknowledges the users who send requests with the wrong or empty token.

Anti CSRF Protection Output

Conclusion

Thus we have implemented the anti-CSRF protection in a PHP contact form.

Hope the example code is useful and you get the implementation process we discussed here.

We have created a SecurityService class in PHP to handle the CSRF protection. It is reusable for several applications wherever you need to enable CSRF protection.

The PHP code that returns response messages acknowledges the user properly.

Download

Comments to “Cross-site Request Forgery (Anti-CSRF) Protection in PHP”

Leave a Reply

Your email address will not be published. Required fields are marked *

↑ Back to Top