Jump to content
bmacnaughton

CSRF problem when AJAX submission

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

Share this post


Link to post
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 3

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

You can stuff the values into $input->post or you can set the HTTP headers, so you can work around it.

  • Like 1

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
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.
 

Share this post


Link to post
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

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Peter Knight
      I have a few web forms which require testing on a weekly basis and I don't want the recipients (administrators) to receive these test emails.
      What would be a good way to test approx 15 forms from the front end and have the test delivered a list of secondary administrator recipients?
      I'm thinking that I could have some kind of config file which watches for a trigger word or email and then understands that it's a test and to bypass the normal admins?
      All of the forms ask for an email address so I could setup an email such as 'testform@email.not' etc which my config file (hook?) would watch for.
      Or is there a better way to do this?
      Additionally, I have a few extra requirements...
      Forms should goto an alternative success page. This is because I don't want my test to skew my Google Analytics conversion tracking Forms would need to be tested from the front-end and not the PW admin area Any advice appreciated.
      BTW I realise this should be posted in the proper FormBuilder support forum. I am in the process of renewing my license for access to that support forum.
       
    • By michelangelo
      Hello there,
      I am building my website, which has a dozen projects with 10 images each. Basically, I need a filtering system but built in the most efficient and user-friendly way. You can see below that the images flow sideways so being hidden, JS lazy loading was a good tool, but I just wanted to try AJAX. Is it fit for this purpose or it's more for dynamic content?
       

    • By louisstephens
      I have been messing around with creating pages from ajax requests, and it has gone swimmingly thus far. However, I am really struggling with creating a page and saving an image via ajax. 
      The form:
      <form action="./" role="form" method="post" enctype="multipart/form-data"> <div> <input type="text" id="preview" name="preview" placeholder="Image Title"> </div> <div> <input type="file" id="preview-name" name="preview-name"> </div> <div> <select id="select-tags" name="select-tags"> <?php $tags = $pages->find("template=tag"); ?> <option value="">Select Your Tags</option> <?php foreach ($tags as $tag) : ?> <option value="<?= $tag->name; ?>"><?= $tag->name; ?></option> <?php endforeach; ?> </select> </div> <div> <button type="button" id="submit-preview" name="submit" class="">Upload Images</button> </div> </form>  
      The ajax in my home template:
      $('#submit-preview').click(function(e) { e.preventDefault(); title = $("#preview").val(); image = $("input[name=preview-name]"); console.log(title); console.log(image); data = { title: title, image: image //not sure if this is actually needed }; $.ajax({ type: 'POST', data: data, url: '/development/upload-preview/', success: function(data) { console.log("Woo"); }, error: function(xhr, ajaxOptions, thrownError) { alert(xhr.responseText); } }); }); And finally in my ajax template:
      $imagePath = $config->paths->assets . "files/pdfs/"; //was from an older iteration $title = $sanitizer->text($_POST['title']); $image = $sanitizer->text($_POST['image']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $p->save(); I can complete the file upload but just using a simple post to the same page and it it works well, but I was really trying to work out the ajax on this so I could utilize some modals for success on creation (and to keep my templates a little cleaner). When I do run the code I have, a new/blank folder is created under assets, and a new page is created with the correct title entered. However, no image is being processed. I do get a 200 status in my console. I have searched google for help, but everything seems to be slightly off from my needs. If anyone could help point me in the right direction I would greatly appreciate it. 
    • By louisstephens
      So I am using ajax to upload an image, but I am getting the error "Method WireUpload:: save does not exist or is not callable". I am not quite sure how to go about fixing this (at the moment).
      elseif($config->ajax && $input->urlSegment1 == "upload-preview") { $u = $config->paths->assets . "files/pdfs/"; $title = $sanitizer->text($_POST['title']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $u->save(); } I compared my code to something I did previously (though previously I just posted to the current template file, not through ajax) which works, but this doesnt seem to be working. I have the _init.php file prepending as well. Does anyone have any ideas of what might be happening?
    • By Brian Scramlin
      I just wanted to share that I added an AJAX-powered gallery to an artist website that I developed and host: https://jackpinecreations.com/gallery/

      There were two things that frustrated me about creating this. Perhaps you can show me a better way.
      1. After creating my processing script, which I placed under /templates/scripts/get-items.php, I realized that I would get a 403, due to ProcessWire's routing and security. This forced me to have to create a template and page for this little script. This was frustrating simply because it seemed unnecessarily confusing. But worse, see #2.
      2. I usually use config.php to prepend and append each of my templates with a head.inc and foot.inc, which keeps my templates easy to use and I don't have to go and use the GUI to do so on each template separately. However, since I realized I needed to create a new template and page so as to access it, whenever I sent POST params to it, I would get the header and footer along with it!!! I could find no workarounds and had to remove the pre/append calls in config.php and use the GUI on each template individually.  
      Code Below if you're interested:
      HTML and JavaScript (forgive my sad JavaScript skills, I know this can be tightened up)
      <!-- Begin Grid --> <div class="container mt-4"> <div id="gallery" class="row"> <?php foreach ($page->children("limit=9") as $child): ?> <div class="col-6 col-md-4 gallery-item"> <a href="<?= $child->url ?>" title="View <?= $child->title ?>"> <img class="gallery-item" src="<?= $child->item_featured_image->size(640, 640)->url ?>" alt="<?= $child->title ?> Image"> </a> </div> <?php endforeach; ?> </div> </div> <!-- End Grid --> <div class="center-block text-center"> <button id="get-more-items" type="button" name="get-more-items" class="btn-vintage">Load More</button> </div> <script type="text/javascript"> var buttonGetItems = document.getElementById("get-more-items"); var indexStart = 0; buttonGetItems.addEventListener("click", function() { indexStart += 9; $.ajax({ url: '<?= $pages->get(1186)->url ?>', type: "POST", dataType:'json', // add json datatype to get json data: ({page_id: <?= $page->id ?>, index_start: indexStart}), success: function(data){ console.log(data); if (data[1]) { //for each element, append it. $.each(data, function(key, value) { $("#gallery").append(value); }); } else { $("#get-more-items").after('<p class="center-block text-center">There are no more items to load.</p>'); $("#get-more-items").remove(); } } }); }); </script> Processing Script
      <?php $items_array = []; $i = 0; foreach ($pages->get($input->post->page_id)->children->slice($input->post->index_start, 9) as $child) { $i++; $items_array[$i] = "<div class='col-6 col-md-4 gallery-item'> <a href='$child->url' title='View $child->title'> <img src='{$child->item_featured_image->size(640,640)->url}' alt='$child->title Image'> </a> </div>"; } echo json_encode($items_array); I love ProcessWire for hundreds of reasons, but I've been using AJAX more and more, and I'm not liking having to create templates to access scripts. 
      Any advice?
×
×
  • Create New...