Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling CSP nonce for scripts between Turbo.visit #294

Open
CiTroNaK opened this issue Jun 28, 2021 · 3 comments
Open

Handling CSP nonce for scripts between Turbo.visit #294

CiTroNaK opened this issue Jun 28, 2021 · 3 comments

Comments

@CiTroNaK
Copy link
Contributor

This issue is related to PR #291 I opened. The solution in the PR does not work properly when a page with the script is loaded for the first from the cache and then replaced with a fresh copy from the server. The first return to the page will work for the preview, as the right CSP nonce will be loaded, but it will fail for the fresh response from the server.

I was able to find out that the browser (I tried the latest Safari, Brave and Firefox) does not accept any changes to the csp-nonce meta tag and will always want the first nonce, that was loaded on the first full page load. Any page visit made by Turbo will try to change the CSP nonce (during PageRenderer#mergeHead), but that will be ignored by the browser.

As it seems, the only solution would be to save the CSP nonce internally when Turbo will be loaded for the first time on the full page load and use it during all visits until another full page load.

Here is a log from the console, where you can see, that the browser want's always the first nonce.

(index):194 hi from script
application.js:31 nonce on page load VsmEI4vz8krfsBpwROOQWQ==
application.js:28 turbo:visit event (another page)
turbo.es2017-esm.js?t=1624876375473:2143 before mergeHead (preview: false) VsmEI4vz8krfsBpwROOQWQ==
turbo.es2017-esm.js?t=1624876375473:2150 after mergeHead (preview: false) Rhb3VbXSQvYu4DSRVMr4hw==
application.js:28 turbo:visit event (the first page)
turbo.es2017-esm.js?t=1624876375473:2143 before mergeHead (preview: true) Rhb3VbXSQvYu4DSRVMr4hw==
turbo.es2017-esm.js?t=1624876375473:2150 after mergeHead (preview: true) VsmEI4vz8krfsBpwROOQWQ==
turbo.es2017-esm.js?t=1624876375473:918 script (preview: true) VsmEI4vz8krfsBpwROOQWQ==
VM2905:4 hi from script
turbo.es2017-esm.js?t=1624876375473:2143 before mergeHead (preview: false) VsmEI4vz8krfsBpwROOQWQ==
turbo.es2017-esm.js?t=1624876375473:2150 after mergeHead (preview: false) Fw31q7Cw30cptXu2TFUEFQ==
turbo.es2017-esm.js?t=1624876375473:918 script (preview: false) Fw31q7Cw30cptXu2TFUEFQ==
turbo.es2017-esm.js?t=1624876375473:2199 Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https: 'unsafe-eval' http://localhost:3036 'nonce-VsmEI4vz8krfsBpwROOQWQ=='". Either the 'unsafe-inline' keyword, a hash ('sha256-LtPpQzFPR+OL7dmrMsTSD2PcJEOV61e9yGaVAeNyRAg='), or a nonce ('nonce-...') is required to enable inline execution.

Would it be a good solution? If yes, could someone point me to a place, where it should be done? I will gladly try to help on this and finish the PR properly 😅

Thank you.

@terracatta
Copy link
Contributor

terracatta commented Jul 11, 2021

Hi everyone, I am just going to post how I solved this with Turbo Drive in my Rails app and also make it work with any legacy UJS. This is a port of similar work I did for the original Turbolinks.

If you want UJS, Turbo, and other inline nonced JS to work you need to do the following:

  1. Change Nonce generation so that nonces do not change for Turbo Drive requests (as you can't count on every part of the DOM being refreshed)
# In config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_nonce_generator = -> (request) do
  # use the same csp nonce for turbo requests
  if request.env['HTTP_TURBO_REFERRER'].present?
    request.env['HTTP_X_TURBO_NONCE']
  else
    SecureRandom.base64(16)
  end
end
  1. Inject a header into Turbo Drive requests so the above nonce generation code works
// Put this somewhere in /app/javascript
document.addEventListener('turbo:before-fetch-request', (event) => {
  // Turbo Drive does not send a referrer like turbolinks used to, so let's simulate it here
  event.detail.fetchOptions.headers['Turbo-Referrer'] = window.location.href
  event.detail.fetchOptions.headers['X-Turbo-Nonce'] = $("meta[name='csp-nonce']").prop('content')
})
  1. Because nonces can only be accessed via their IDL attribute after the page loads (for security reasons), they need to be read via JS and added back as normal attributes in the DOM before the page is cached otherwise on cache restoration visits, the nonces won’t be there!
// Put this somewhere in /app/javascript
document.addEventListener("turbo:before-cache", function() {
  let scriptTagsToAddNonces = document.querySelectorAll("script[nonce]");
  for (var element of scriptTagsToAddNonces) {
    element.setAttribute('nonce', element.nonce);
  }
});

If done correctly you should not have any error messages about inline nonced JS not running due to your CSP policy. Keep in mind that while this approach works, this arguably weakens the security of the nonce generation because a single nonce could last for a long time for your average visitor (until they do a normal non-turbodrive navigation or reload the page).

That said, IMO it's much better to have this, than to have no CSP at all.

@dorianmariecom
Copy link

dorianmariecom commented Dec 17, 2021

edit: doesn't fix my issue with Safari when injecting scripts in the <head>

without the referrer and without jQuery:

document.addEventListener("turbo:before-fetch-request", (event) => {
  event.detail.fetchOptions.headers["X-Turbo-Nonce"] =
    document.querySelector("meta[name='csp-nonce']")?.content
})

document.addEventListener("turbo:before-cache", () => {
  document.querySelectorAll("script[nonce]").forEach((element) => {
    element.setAttribute("nonce", element.nonce);
  })
})
  config.content_security_policy_nonce_generator =
    lambda do |request|
      request.env["HTTP_X_TURBO_NONCE"].presence ||
        request.session.id.presence || SecureRandom.hex
    end

@DavidPetrasek
Copy link

@terracatta's solution works and I have to add that in my case the third step is not necessary, because the restoration visits already contain all nonces and it shows no CSP violations. I'm using Symfony 7 together with Symfony UX Turbo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants