1

My Rails app features a public-facing form that passes user input to the controller as a stringified JSON via AJAX. The form is designed for offline use, and so every visit to the form page other than the first is served from the browser cache (using the cache manifest). I am having an issue where the form submission returns a 422 unprocessable entity error unless the browser history has been cleared before navigating to the form page... that is to say that a user can only make one form submission, all subsequent submissions are 422 unless they clear the history and return to the form to refresh the cache. Unfortunately, that's not going to fly.

I am not tremendously experienced with Rails security, but I am under the impression that this has to do with CSRF protection and the fact that, for any visit to the form page other than the first, a stale CSRF token is being passed.

My AJAX request appears like so:

$.ajax({
    url: "post/submission",
    type: "POST",
    dataType: "json",
    beforeSend: function(xhr) {xhr.setRequestHeader("X-CSRF-Token", $("meta[name='csrf-token']").attr("content"))},
    data: {"post" : postParameter},
    success: function(response){
        window.location = '/post/approval';
    }
});

At the moment, the layout page includes the <%= csrf_meta_tags %>, and I have the standard protect_from_forgery with: :exception in the application controller.

The final structural element to note about this form is that, although the form itself is public-facing, it requires a user login after the submit button is clicked - so a submission will not be successful without a valid login.

Is there a safe way that I can get around this problem? I'm sure it goes without saying, but I can't have my users clearing their history and re-caching the form after every submission.

skwidbreth
  • 7,888
  • 11
  • 58
  • 105
  • If you want to keep CSRF protection, you have to regenerate the token when presenting the form. `<%= hidden_field_tag :authenticity_token, form_authenticity_token %>` does that. An ajax call when the page is shown from cache would do. See more [here](http://stackoverflow.com/questions/8503447/rails-how-to-add-csrf-protection-to-forms-created-in-javascript) and [here](http://stackoverflow.com/questions/829046/how-do-i-detect-if-a-user-has-got-to-a-page-using-the-back-button). – Nic Nilov Jun 28 '16 at 13:20
  • Hmm... one other thing that I should've mentioned is that the form is not built in the standard Rails fashion with form helpers, etc. In fact, the form is not a form in the HTML sense at all - on submit, user input is is compiled into a JSON string and AJAX'd to the controller to be parsed out and inserted into the DB... so there's not really a place to put a `hidden_field_tag`. I think there may be some good solutions for me in the second link you included - I'll test them out and let you know if I can get anything going. Thank you for your time. – skwidbreth Jun 28 '16 at 13:37
  • Sure, you just need to pass the result of `form_authenticity_token` to your frontend, regardless whether it is done as a part of template rendering or transported via ajax call. From there you will be able to include it to your POST payload. – Nic Nilov Jun 28 '16 at 13:39
  • Ok thank you - let me see what I can work out. – skwidbreth Jun 28 '16 at 14:45
  • @NicNilov I added the hidden auth token to the page and modified the AJAX so that the url was `"post/submission" + "?&authenticity_token=" + auth_token` (`auth_token` being the value of $("#authenticity_token")), but it still was throwing the same error. Is this due to the form being served out of the cache? I also tested out setting `protect_from_forgery with: :null_session`. That did work - is there any security reason not to do this? – skwidbreth Jun 28 '16 at 15:54
  • `protect_from_forgery with: :null_session` working makes sense since Rails just drops the session in case auth token doesn't match and continues with the request. You need to refresh your auth token from backend when the page is restored from browser cache. One of the links above show how to detect that event. – Nic Nilov Jun 28 '16 at 15:58
  • Ok thanks man, you're the best! – skwidbreth Jun 28 '16 at 16:12
  • Just thought, why not have an answer accepted then :) – Nic Nilov Jun 28 '16 at 17:15

1 Answers1

2

In order to keep CSRF protection while submitting POST requests from JavaScript you need to supply current authentication token either in the form's payload or in request headers.

When the page is loaded from browser cache its authentication token may be stale, so it needs to be refreshed from backend. One way to do that would be to detect "page loaded from browser cache" event and run an ajax request to fetch the new token which should be generated by form_authenticity_token method on backend.

The received authentication token can then be used for subsequent JavaScript POST requests.

A technique of detecting the page being loaded from cache is described e.g. in this StackOverflow answer.

Community
  • 1
  • 1
Nic Nilov
  • 5,056
  • 2
  • 22
  • 37