Content Security Policy (CSP) adds a powerful security layer to prevent Cross Site Scripting (XSS) attacks. Rails makes CSP easy by generating a nonce (random token per request) that you attach to inline scripts. This ensures only scripts generated by your server can run โ attackers can’t inject HTML/JS and execute it.
A nonce is a number used once โ a unique per-request token (e.g. nonce-faf83af82392). Add it to inline <script> tags:
<script nonce="<%= content_security_policy_nonce %>">
// safe inline script
</script>
The browser only runs the script if the nonce matches the value advertised in the Content-Security-Policy response header.
Rails ships with a CSP initializer. Edit or create config/initializers/content_security_policy.rb and configure script_src to allow nonces:
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.style_src :self, :https
policy.script_src :self, :https, :nonce # allow scripts with nonce
end
# Optional: report violations
# policy.report_uri "/csp-violation-report-endpoint"
end
Restart your server after changing initializers.
Example app/views/layouts/application.html.erb using csp_meta_tag and per-request nonce:
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<%= csp_meta_tag %>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
</head>
<body>
<%= yield %>
<!-- inline script with nonce -->
<script nonce="<%= content_security_policy_nonce %>">
console.log("CSP Nonce example script");
</script>
</body>
</html>
Only this inline script will run; any injected script without the nonce will be blocked by the browser.
Turbo works fine with CSP nonces because Turbo prefers external script modules and does not require inline JavaScript. If you must return inline JS in Turbo responses (e.g., Turbo Streams that embed inline <script>), include the nonce on those scripts.
Stimulus controllers are loaded from external JavaScript (via import maps, webpack, or esbuild) โ so they do not require inline scripts and thus are fully compatible with strict CSP policies. If you initialize Stimulus inline (not recommended), add the nonce:
<script nonce="<%= content_security_policy_nonce %>">
// initialize application controllers
</script>
Best practice: keep all JS in app/javascript/controllers and avoid inline JS.
If you need to do a redirect from an HTML fragment returned to a Turbo frame, you might inline a small script. Add nonce like this:
<!-- returned inside a turbo frame response -->
<script nonce="<%= content_security_policy_nonce %>">
Turbo.visit("<%= dashboard_path %>");
</script>
Open browser DevTools โ Network โ select a request โ Response Headers. You should see something like:
Content-Security-Policy: script-src 'self' https: 'nonce-<value>'
You can also read the header in Rails tests or logs.
Verify forms include authenticity token (CSRF):
require "rails_helper"
RSpec.describe "CSRF token", type: :system do
it "includes authenticity token in forms" do
visit new_user_registration_path
expect(page.html).to match(/name="authenticity_token"/)
end
end
You can assert that the response includes a nonce and that an inline script was rendered with that nonce:
require "rails_helper"
RSpec.describe "CSP Nonce", type: :request do
it "adds nonce to inline scripts" do
get root_path
csp_header = response.headers["Content-Security-Policy"]
# extract nonce value from header (pattern depends on how you configured policy)
nonce = csp_header[/nonce-(.*?)'/, 1]
# ensure the body includes a script tag with the nonce
expect(response.body).to include("nonce=\"#{nonce}\"")
end
end
Note: header parsing may require adjusting the regex depending on quoting in your CSP header.
- Avoid inline JS where possible โ favors well-structured JS bundles.
- Use nonces only when necessary (e.g., third-party scripts that are injected inline, small inline initializers returned in Turbo responses).
- Test in production-like environment because browsers enforce CSP differently; dev tooling can be permissive.
- Report-only mode during rollout: set
policy.report_only = trueto collect violations without blocking.
- If inline scripts still blocked: confirm
csp_meta_tagis present and that the inline<script>usescontent_security_policy_nonce. - For external scripts blocked: ensure
script_srcincludes the allowed host orhttps:. - If using
secure_headersgem or a reverse proxy adding headers, ensure they donโt conflict.
- Enable CSP with nonces for inline scripts when necessary.
- Prefer Stimulus/Turbo with external JS modules โ avoid inline code.
- Test CSP and CSRF behavior in request/system specs.
- Use report-only mode when rolling out strict CSP.
Quick checklist
- Add
policy.script_src :self, :https, :nonceincontent_security_policy.rb - Use
<%= csp_meta_tag %>in layout - Add
nonce="<%= content_security_policy_nonce %>"to inline scripts - Move JS into
app/javascript/controllerswhere possible - Add request/system specs to validate nonce and CSRF