Jump to content

CSRF problem when AJAX submission


bmacnaughton
 Share

Recommended Posts

I am implementing the ability to handle form submission using AJAX. The problem I have is that even though, as far as I can tell, I convert the AJAX-submitted JSON input into the equivalent of $input->post. When I call $form->processInput() it always throws that it appears to be forged.

 

if ($config->ajax && $_SERVER['REQUEST_METHOD'] === 'POST') {
    // get the file body and decode the JSON.
    try {
        $body = file_get_contents('php://input');
        $json = wireDecodeJSON($body);
        //wire('log')->save('info', 'ajax-json:' . print_r($json, true));
        wire('log')->save('info', 'json-data:' . print_r($json['data'], true) . ' sid: ' . session_id());
        $fakeinput = new WireInputData($json['data']);
        $form = deleteRequest('xyzzy');

        $form->processInput($fakeinput);
    } catch (Exception $e) {
        http_response_code(404);
        echo json_encode(array('reason' => $e->getMessage()));
    }
    return;

 

The log shows that the TOKEN and the TOKEN value are the same as when a normal form is submitted (I have both on the page for testing and can submit via normal POST as well as via AJAX). The session_id() value is the same. What am I missing?

 

Log entries ("dumb" is the normal submit button name, "fake-text" is an empty text field, "submit" is the AJAX submit button name.)

(using form-post): post Array ( [dumb] => dumb-button [TOKEN1649939534X1479234443] => f4VLZ17RlXfp9KVCQr/GIhoZ3krbuWK5 )

(using ajax-post): json-data: Array ( [fake-text] => [submit] => DELETE ENDUSERS [TOKEN1649939534X1479234443] => f4VLZ17RlXfp9KVCQr/GIhoZ3krbuWK5 ) 

Edited by bmacnaughton
Typos and clarification
Link to comment
Share on other sites

OK, I think I know what the problem is.

SessionCSRF.hasValidToken() doesn't use the argument passed to $form->processInput() to get the CSRF token. It references $input->post no matter what was passed to processInput(). And with an AJAX request $input->post is empty; it seems like it should use the argument passed to $form->processInput(). (Actually, it checks for extended HTTP headers when it's an AJAX request but if they aren't present it then directly references $input->post.)

The solution is to set an HTTP header X-tokenname where tokenname is the CSRF token name and the value for the header is the CSRF token value.

It's odd the $form->processInput($arg) does take field values from $arg but it doesn't take the token from $arg and instead directly references $input->post.

Edited by bmacnaughton
Clarification/additional information
  • Like 4
Link to comment
Share on other sites

  • 5 months later...
11 hours ago, hettiger said:

Is there really no solution to this issue? It's a huge drawback thought :-/

That's how php is supposed to work, and processwire does not deviate from that. 

Quote

$_POST: An associative array of variables passed to the current script via the HTTP POST method when using application/x-www-form-urlencoded or multipart/form-data as the HTTP Content-Type in the request.

source

If one does need a different behaviour it's their own responsibility. And to be honest, if one is using processwire's form api with custom ajax sending, then it shouldn't be that hard to build that custom ajax handler to send the form data as application/x-www-form-urlencoded or multipart/form-data.

  • Like 1
Link to comment
Share on other sites

5 hours ago, LostKobrakai said:

That's how php is supposed to work, and processwire does not deviate from that

What's odd is that PW doesn't take all the data from the array passed to $form->process().  You can argue the CSRF protection is a different function, but that's just internal implementation. And it's easy enough to stuff things into $input->post, so it's not like it prevents forging.
 

Link to comment
Share on other sites

Thank you guys for the additional information. What I ended up is posting data like this:

private drop(event: DragEvent): void {
    let payload = JSON.parse(event.dataTransfer.getData("application/json"));
    let widget = this.$root as any;
    let data = new URLSearchParams();

    // ProcessWire CSRF protection does not recognize JSON data per default
    data.append(widget.tokenName, widget.tokenValue);
    data.append('foo', payload.foo);

    fetch("/admin/page/foo/bar/", {
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "X-Requested-With": "XMLHttpRequest" // Required for ProcessWire to identify as ajax request
        },
        method: "post",
        credentials: "same-origin",
        body: data
    })
    .then(response => response.text())
    .then(text => console.log(text))
    .catch(error => console.error(error));
}

This request is perfectly recognized by ProcessWire the way you would expect and CSRF protection is working.

Quote

LostKobrakai

If one does need a different behaviour it's their own responsibility. And to be honest, if one is using processwire's form api with custom ajax sending, then it shouldn't be that hard to build that custom ajax handler to send the form data as application/x-www-form-urlencoded or multipart/form-data.

It shouldn't be tedious to make use of security critical features like CSRF protection.

I love ProcessWire because it saves me huge amounts of time. If I have to mess around with something to make it work that costs me time. It doesn't matter if you consider it to be an easy task or if it's the developer's responsibility. It takes time and there's room for improvements. I'm just giving feedback. Take it or leave it.

  • Like 2
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...