src/CSRF.php
<?php
namespace Athens\CSRF;
/**
* Class CSRF provides methods for protection against CSRF attacks.
*
* @package Athens
*/
class CSRF
{
const CSRF_TOKEN_HEADER = 'CSRF-TOKEN';
/**
* A list of the "unsafe" HTTP methods for which we shall require a valid CSRF token.
*
* @var string[]
*/
protected static $unsafe_methods = ["POST", "PUT", "PATCH", "DELETE"];
/**
* Initialize CSRF token security.
*
* Checks if a CSRF token is required for this request, and if one has been provided.
* Inserts a CSRF token into any form, and inserts a javascript CSRF_TOKEN variable.
*
* @return void
* @throws \Exception If no CSRF token is provided when one is required.
*/
public static function init()
{
// Insert a CSRF token into the session
static::generateToken();
// Checks if a CSRF token is required for this request, and, if so, whether the
// correct one is present
static::checkCSRF();
// Begin output buffering, with callback to insert our CSRF tokens into the page
ob_start(static::generateCallback(static::getToken()));
}
/**
* @return void
*/
protected static function generateToken()
{
if (isset($_SESSION['csrf_token']) === false) {
$_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes(16));
}
}
/**
* @return string
*/
protected static function getToken()
{
return $_SESSION['csrf_token'];
}
/**
* @param string $token
* @return callable
*/
protected static function generateCallback($token)
{
return function ($page) use ($token) {
$tokenField = "\n<input type=hidden name=csrf_token value=$token>\n";
$tokenJS = "\n<script>var CSRFTOKEN = '$token';</script>\n";
if (strpos(strtolower($page), "<head>") !== false) {
$page = substr_replace($page, "<head>" . $tokenJS, strpos(strtolower($page), "<head>"), 6);
}
$matches = [];
if (preg_match_all('/<\s*\w*\s*form.*?>/is', $page, $matches, PREG_OFFSET_CAPTURE) !== 0) {
foreach ($matches[0] as $match) {
$formOpen = strpos($page, $match[0], $match[1]);
$formClose = strpos($page, ">", $formOpen);
$formTag = substr($page, $formOpen, $formClose-$formOpen);
$formIsMethodGet = stripos(str_replace(['"', "'"], ["", ""], $formTag), "method=get") !== false;
if ($formIsMethodGet !== true) {
$page = substr_replace($page, $tokenField, $formClose + 1, 0);
}
}
}
return $page;
};
}
/**
* Get the CSRF token, presented as a header, a PUT parameter, or a POST parameter.
*
* @return string|null
*/
protected static function getSuppliedCSRF()
{
$headers = function_exists('getallheaders') === true ? getallheaders() : [];
$requestArguments = [];
parse_str(file_get_contents('php://input'), $requestArguments);
$requestArguments = array_merge($_POST, $requestArguments);
if (array_key_exists(static::CSRF_TOKEN_HEADER, $headers) === true) {
return $headers[static::CSRF_TOKEN_HEADER];
} elseif (array_key_exists("csrf_token", $requestArguments) === true) {
return $requestArguments['csrf_token'];
} else {
return null;
}
}
/**
* Predicate which reports whether a CSRF token is required for the present request.
*
* @return boolean
*/
protected static function csrfIsRequired()
{
return in_array($_SERVER['REQUEST_METHOD'], static::$unsafe_methods);
}
/**
* Returns the expected CSRF token in the current session.
*
* @return string|null
*/
protected static function getSessionCSRF()
{
return array_key_exists('csrf_token', $_SESSION) === true ? $_SESSION['csrf_token'] : null;
}
/**
* @throws \Exception If the CSRF Token has not been set, is missing from the submission, or incorrect.
* @return void
*/
protected static function checkCSRF()
{
if (static::getSessionCSRF() === null) {
throw new \Exception('No CSRF Token set in $_SESSION. Invoke \Athens\CSRF\CSRF::init before ::checkCSRF');
}
if (static::csrfIsRequired() === true && static::getSuppliedCSRF() !== static::getToken()) {
if (headers_sent() === false) {
header("HTTP/1.0 403 Forbidden");
}
echo "Page error: CSRF token missing or incorrect. If this problem persists, " .
"please contact the page administrator.\n";
throw new \Exception("CSRF token missing or incorrect. Ensure that you " .
"are using Athens\\CSRF\\CSRF::init() to insert the CSRF token into " .
"submitted forms, and that any AJAX submission methods include the CSRF" .
"javascript variable.");
}
}
}