Skip to content

Using CSRF protection with AJAX doesn't work as expected #250

@abdusco

Description

@abdusco

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>
chrome_2017-04-23_20-41-09

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 using fetch() api with custom headers to simulate AJAX request
    on(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 using session()->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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions