-
Notifications
You must be signed in to change notification settings - Fork 91
Add a Report-Only Content-Security-Policy baseline #464
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
Open
jeremy
wants to merge
4
commits into
main
Choose a base branch
from
security/xss-campaign-writebook-csp-report-only
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8f3ec57
Add a Report-Only Content-Security-Policy baseline
jeremy 9ea7017
Add a session-stable CSP nonce and complete the directive set
jeremy fe1e661
Make CSP embed directives configurable per install
jeremy c8e13ba
Tokenize CSP_EXTRA_* semicolons to avoid a per-request 500
jeremy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,73 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
|
|
||
| # Define an application-wide content security policy. | ||
| # Baseline application-wide Content-Security-Policy, deployed in Report-Only | ||
| # mode: browsers evaluate the policy and report violations (once a report | ||
| # endpoint is wired up) but enforce nothing, so rendering cannot break. | ||
| # Tune the policy against observed violations, then flip | ||
| # `content_security_policy_report_only` off to enforce it. | ||
| # | ||
| # See the Securing Rails Applications Guide for more information: | ||
| # https://guides.rubyonrails.org/security.html#content-security-policy-header | ||
|
|
||
| # Rails.application.configure do | ||
| # 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 | ||
| # Session-stable nonce for permitted inline scripts (the importmap JSON + shim, | ||
| # auto-nonced by importmap-rails). | ||
| # | ||
| # # Generate session nonces for permitted importmap, inline scripts, and inline styles. | ||
| # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } | ||
| # config.content_security_policy_nonce_directives = %w(script-src style-src) | ||
| # The nonce is stable across a session's requests so Turbo snapshot restores | ||
| # don't replay a stale nonce and trip CSP. It's the HMAC of a stable cookie | ||
| # value keyed by the server secret: the cookie is client-settable, but the | ||
| # client can't predict the resulting nonce without knowing secret_key_base. | ||
| # | ||
| # # Report violations without enforcing the policy. | ||
| # # config.content_security_policy_report_only = true | ||
| # end | ||
| # Writebook sets no per-session verification cookie, so the lightweight | ||
| # nonce_id cookie — set on first visit, present for every session including | ||
| # unauthenticated ones — is the sole identifier. | ||
| module CSP | ||
| module Nonce | ||
| COOKIE = "writebook_csp_nonce_id" | ||
|
|
||
| def self.generate(request) | ||
| hmac(nonce_id(request)) | ||
| end | ||
|
|
||
| def self.hmac(identifier) | ||
| OpenSSL::HMAC.hexdigest("SHA256", Rails.application.secret_key_base, identifier) | ||
| end | ||
|
|
||
| # Read or initialize a stable nonce identifier cookie. | ||
| def self.nonce_id(request) | ||
| request.cookies[COOKIE] || set_nonce_id(request) | ||
| end | ||
|
|
||
| def self.set_nonce_id(request) | ||
| value = SecureRandom.base64(16) | ||
| request.cookie_jar[COOKIE] = { value: value, httponly: true, same_site: :lax } | ||
| value | ||
| end | ||
| end | ||
| end | ||
|
|
||
| Rails.application.configure do | ||
| config.content_security_policy do |policy| | ||
| policy.default_src :self | ||
| policy.script_src :self # nonce auto-appended via nonce_directives below | ||
| # unsafe_inline retained: many style="…" attributes and the per-user | ||
| # hide_from_user_style_tag can't be nonced yet. | ||
| policy.style_src :self, :unsafe_inline | ||
| # frame_src / img_src / connect_src start at :self and get tuned against | ||
| # violation reports during the report-only window. | ||
| policy.img_src :self, :data, :blob | ||
| policy.connect_src :self | ||
| policy.frame_src :self | ||
| policy.frame_ancestors :self | ||
| policy.base_uri :self | ||
| policy.form_action :self | ||
| policy.object_src :none | ||
| # Specify URI for violation reports once a report sink is available | ||
| # policy.report_uri "/csp-violation-report-endpoint" | ||
| end | ||
|
|
||
| config.content_security_policy_nonce_generator = ->(request) { CSP::Nonce.generate(request) } | ||
| config.content_security_policy_nonce_directives = %w[ script-src ] | ||
|
|
||
| # Report violations without enforcing the policy. | ||
| config.content_security_policy_report_only = true | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| require "test_helper" | ||
|
|
||
| class CspNonceTest < ActionDispatch::IntegrationTest | ||
| test "policy is delivered Report-Only, not enforced" do | ||
| sign_in :david | ||
| get root_url | ||
|
|
||
| assert_response :success | ||
| assert response.headers["Content-Security-Policy-Report-Only"].present?, | ||
| "Expected a Report-Only CSP header" | ||
| assert_nil response.headers["Content-Security-Policy"], | ||
| "Policy must not be enforced yet" | ||
| end | ||
|
|
||
| test "nonce is stable across requests so Turbo restores don't trip CSP" do | ||
| sign_in :david | ||
|
|
||
| get root_url | ||
| nonce1 = report_only_nonce | ||
|
|
||
| get root_url | ||
| nonce2 = report_only_nonce | ||
|
|
||
| assert nonce1.present?, "Expected a nonce in the Report-Only CSP header" | ||
| assert_equal nonce1, nonce2, "Nonce must be stable across requests" | ||
| end | ||
|
|
||
| test "client-set identifier still yields an unpredictable HMAC nonce" do | ||
| fake_id = "attacker-controlled-value" | ||
| cookies[CSP::Nonce::COOKIE] = fake_id | ||
|
|
||
| get root_url | ||
| nonce = report_only_nonce | ||
|
|
||
| assert_equal CSP::Nonce.hmac(fake_id), nonce, | ||
| "Nonce must be HMAC-SHA256 of the identifier keyed by secret_key_base" | ||
| end | ||
|
|
||
| test "importmap script tag carries the nonce" do | ||
| sign_in :david | ||
| get root_url | ||
|
|
||
| assert_response :success | ||
| nonce = report_only_nonce | ||
| assert_select "script[type='importmap'][nonce=?]", nonce | ||
| end | ||
|
|
||
| private | ||
| def report_only_nonce | ||
| response.headers["Content-Security-Policy-Report-Only"].to_s[/'nonce-([^']+)'/, 1] | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.