-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Short description of the issue
I made a forum post about this issue, but didn't get any tangible response, so I'll try my chances here.
Context: I am building a public-facing API using ProcessGraphQL module. I made a contact form, and want to implement some progressive enhancements.
I'm trying to use CSRF protection with AJAX requests. I echo a hidden field using <?= session()->CSRF->renderInput('contact') ?>
inside the form, and check it on the backend using session()->CSRF->hasValidToken('contact')
. When I'm not using AJAX, with plain POST requests, it works just fine, token validates successfully.
With AJAX, I send the token id and value manually with the header X-<TOKENID>:<TOKEN VALUE>
and explicitly set X-Requested-With
header to force AJAX detection, so that this section from SessionCSRF.php could intercept and validate the token. PHP recognizes the headers, $config->ajax
returns true but hasValidToken()
fails nevertheless.
public function hasValidToken($id = '') {
$tokenName = $this->getTokenName($id);
$tokenValue = $this->getTokenValue($id);
// ...
if($this->config->ajax && isset($_SERVER["HTTP_X_$tokenName"]) && $_SERVER["HTTP_X_$tokenName"] === $tokenValue) return true;
if($this->input->post($tokenName) === $tokenValue) return true;
// if this point is reached, token was invalid
return false;
}
Because, after some debugging inside SessionCSRF
class, I found out that with each request $this->getTokenName($id){}
returns a new token name, because it cannot find inside $_SESSION global the token I'm sending back.
public function getTokenName($id = '') {
$tokenName = $this->session->get($this, "name$id");
if(!$tokenName) {
$tokenName = 'TOKEN' . mt_rand() . "X" . time(); // token name always ends with timestamp
$this->session->set($this, "name$id", $tokenName);
}
return $tokenName;
}
I'm not using a private tab or anything, and admin user stays logged in, so $_SESSION seems to work correctly, but apparently not when it's checking CSRF token.
Expected behavior
getTokenName($id = ''){}
returns the token name that's set when building the hidden <input>
Actual behavior
It returns a new token, which invalidates the previous one. There's no redirects caused by missing slashes from request URI or anything.
Steps to reproduce the issue
- Build a
<form action="/api/" method="post">
, echo hidden input using<?= session()->CSRF->renderInput('contact') ?>
- Using JS get token name and value from the hidden input and set
X-<TOKENNAME>:<TOKENVALUE>
headers for AJAX request. I'm usingfetch()
api with custom headers to simulate AJAX requeston(form, 'submit', (e) => { e.preventDefault(); let data = { ... }; // set correct headers let headers = {}; headers['X-' + token.name] = token.value; headers['X-Requested-With'] = 'XMLHttpRequest'; let query = `mutation{contact(${arg(data)}){statusCode, messages}}`; fetchival('/api/', {headers}) .post({query}) .then((result) => { // success }).catch((result) => { // fail }); });
- Inside
/site/templates/api.php
, try to validate token usingsession()->CSRF->hasValidToken('contact')
or some other id that is set before.
Setup/Environment
ProcessWire: 3.0.61
PHP: 7.0.15-0ubuntu0.16.04.4
Apache: Caddy/0.9.4
MySQL: 5.7.17-0ubuntu0.16.04.1
allow_url_fopen: 1
max_execution_time: 400 (changeable)
max_input_nesting_level: 64
max_input_time: 200
max_input_vars: 1000
memory_limit: 256M
post_max_size: 100M
upload_max_filesize: 200M
ProcessGraphQL: 0.16.0
TracyDebugger: 4.3.3
- ProcessWire version:
- (Optional) PHP version:
- (Optional) MySQL version:
- (Optional) Any 3rd party modules that are installed and could be related to the issue: