Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Respond with *.js.erb using nonce strategy for CSP

I'm implementing a CSP using rails 5.2.1 content security policy DSL. I've got my policy set to something like:

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.connect_src :self
  #...
  policy.script_src  :self
end

# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

I also have <%= csp_meta_tag %> in my application.html.erb

At this point I need to add a nonce: true flag to any inline scripts for these to satisfy the policy. I've done this and it works as expected. However, I'm having trouble maintaining existing AJAX style functionality. For example, I have something like (note the remote: true):

# index.html.erb
<%= link_to create_object_path, id: "#{object.code}",method: :post, remote: true do %>
<button type="button">Create object</button>
<% end %>

In my controller

def create
  @object = current_user.object.create
  respond_to do |format|
    if @object
      format.js
    else
      redirect_back
      format.html
    end
  end
end

In my *.js.erb file

$("#<%= @object.service.id %>").text("Added!");

The object is successfully created but I believe the policy is blocking the above "Added" success message that I add to the DOM. I have not seen any errors in the console so I'm not sure where to go from here.

My understanding in this scenario is script tags are temporarily inserted with the contents of the *.js.erb file and these script tags do not contain the nonce. Or, it is a mismatch.

I've been stuck on how to troubleshoot from here. Any guidance here is much appreciated even if different architectural pattern for sending data to client is the way forward. Thanks in advance.

like image 578
computer_smile Avatar asked Jan 03 '19 05:01

computer_smile


1 Answers

I ran into a similar issue. In my case, it didn't refuse to run the js.erb file itself but rather scripts in templates nested within that file through the use of render. So, this answer may have limited utility to your specific case. That said, I did try to reproduce your issue using Rails version 6.1.1 and couldn't.

However, even if you get past the initial hurdle of getting just your .js.erb file to run, you can still run into the issue of nested scripts: if your .js.erb file renders a template that contains a script tag. That script won't run because the request from which it originated assigns it a new nonce, which won't match the nonce in the meta tag.

So, to those coming here from a search engine as I did, here's the general strategy I pursue to get async embedded JS working with CSP for that nested case and assuming the .js.erb file itself runs. Using your case as an example:

  1. Send the nonce along in the AJAX request. I suppose you won't get around writing some custom JS to send the request. Something like:

    document.getElementById('<%= object.code %>').addEventListener('click', e => {
      e.preventDefault(); // So we don't send two requests
    
      fetch('<%= create_object_path %>', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=utf-8'
        },
        body: JSON.stringify({
          nonce: document.getElementsByName('csp-nonce')[0].content
        })
      });
    });
    

    This sends the nonce from the meta tag to the server in the form of a nonce parameter.

    You may need to remove remote: true from your link for this to work. And of course, this script will itself need to be 'nonced' or else it won't run!

  2. Assign the nonce to a @nonce instance variable in the controller:

    @nonce = params[:nonce]
    
  3. Wherever you render scripts, do:

    <%= javascript_tag nonce: @nonce || true do %>
      ...
    

For those wondering how to get the same to work with their existing asynchronous forms:

  1. Add this form field: <%= hidden_field_tag :nonce %>

  2. On form submit, assign the nonce from the meta tag to the hidden field:

    document.getElementById('id_of_submit_button').addEventListener('click', async e => {
      document.getElementById('nonce').value = document.getElementsByName('csp-nonce')[0].content;
    });
    

    In this case, you don't want to prevent the default behavior on the event because you want the form to submit.

Then continue with step 2 above (assigning the nonce to a controller instance variable).

I hope as a general strategy this is useful to some. And I hope it can serve as inspiration for how to get the .js.erb file itself to run.

UPDATE: Of course, for your specific (but limited) use case, you could simply return the object's service id as part of some JSON object you return to the client instead of rendering a .js.erb template. I say "limited" because this won't work for people who really need to render templates.

If you did want to render your .js.erb file, I suspect something like this could work for your case as well, where instead of checking whether the HTTP_TURBOLINKS_REFERRER header is present, you check for request.xhr?. Just know that starting in newer Rails versions, remote: true doesn't set the requisite header for request.xhr? to work anymore. But since you're on 5.2.1, it may work for you.

like image 195
weltschmerz Avatar answered Nov 03 '22 00:11

weltschmerz