-
Notifications
You must be signed in to change notification settings - Fork 8
security: config hardening — CORS, CSP, master_key, Sidekiq, DNS rebinding, profiler, Lookbook #1516
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
base: main
Are you sure you want to change the base?
security: config hardening — CORS, CSP, master_key, Sidekiq, DNS rebinding, profiler, Lookbook #1516
Changes from 8 commits
e06d16a
91dab75
1741550
8a2493b
c7a9fa8
e2e654f
5ed9d15
57b10c1
70148ae
4420103
f401002
6c92863
f3d11f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ | |
|
|
||
| # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment | ||
| # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). | ||
| # config.require_master_key = true | ||
| config.require_master_key = true | ||
|
|
||
| # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. | ||
| # config.public_file_server.enabled = false | ||
|
|
@@ -102,6 +102,7 @@ | |
| config.active_record.dump_schema_after_migration = false | ||
|
|
||
| # Enable DNS rebinding protection and other `Host` header attacks. | ||
| config.hosts = [ ENV["APP_DOMAIN"] ].compact if ENV["APP_DOMAIN"].present? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Restricting Useful? React with 👍 / 👎. |
||
| # config.hosts = [ | ||
| # "example.com", # Allow requests from example.com | ||
| # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,71 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
|
|
||
| # Define an application-wide content security policy. | ||
| # 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.script_src :self, :https, 'https://us.i.posthog.com' | ||
| # policy.style_src :self, :https | ||
| # # Specify URI for violation reports | ||
| # # policy.report_uri "/csp-violation-report-endpoint" | ||
| # end | ||
| # | ||
| # # 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) | ||
| # Content Security Policy — report-only mode. | ||
| # Violations are logged to /csp-violation-report but nothing is blocked yet. | ||
| # Once violations are reviewed and the policy is tuned, switch to enforcement | ||
| # by removing config.content_security_policy_report_only = true. | ||
| # | ||
| # # Report violations without enforcing the policy. | ||
| # # config.content_security_policy_report_only = true | ||
| # end | ||
| # Known external services that need allowlisting: | ||
| # - PostHog: https://us.i.posthog.com, https://us-assets.i.posthog.com | ||
| # - Plaid: https://cdn.plaid.com | ||
| # - Stripe: https://js.stripe.com, https://hooks.stripe.com | ||
| # - Pusher: wss://*.pusher.com (ActionCable / Hotwire) | ||
| # - OpenAI: (server-side only, no browser resources needed) | ||
|
|
||
| Rails.application.configure do | ||
| config.content_security_policy do |policy| | ||
| policy.default_src :self | ||
| policy.font_src :self, :data | ||
| policy.img_src :self, :https, :data, :blob | ||
| policy.object_src :none | ||
|
|
||
| managed_mode = Rails.application.config.app_mode.managed? | ||
|
|
||
| # Scripts: self + (optional) PostHog + Plaid + Stripe + importmap inline (nonce-controlled) | ||
| script_src = [ | ||
| :self, | ||
| "https://cdn.plaid.com", | ||
| "https://js.stripe.com" | ||
| ] | ||
|
|
||
| # Connections: self + external APIs used client-side | ||
| connect_src = [ | ||
| :self, | ||
| "https://hooks.stripe.com" | ||
| ] | ||
|
|
||
| if managed_mode | ||
| script_src += [ | ||
| "https://us.i.posthog.com", | ||
| "https://us-assets.i.posthog.com" | ||
| ] | ||
|
|
||
| connect_src += [ | ||
| "https://us.i.posthog.com", | ||
| "https://us-assets.i.posthog.com", | ||
| "wss://*.pusher.com" | ||
| ] | ||
| end | ||
|
|
||
| policy.script_src(*script_src) | ||
|
|
||
| # Styles: self + unsafe_inline needed for Tailwind/inline styles | ||
| policy.style_src :self, :unsafe_inline | ||
|
|
||
| policy.connect_src(*connect_src) | ||
|
|
||
| # Frames: Plaid and Stripe use iframes | ||
| policy.frame_src "https://cdn.plaid.com", | ||
| "https://js.stripe.com" | ||
|
|
||
| policy.report_uri "/csp-violation-report" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Expect: a route and controller/action that accepts CSP violation reports.
rg -n -C3 'csp-violation-report|csp.?violation|content_security_policy.*report'Repository: we-promise/sure Length of output: 4490 🏁 Script executed: #!/bin/bash
# Search for routes that handle CSP violations
find . -name "routes.rb" -type f | head -5 | xargs cat | grep -n -i "csp\|violation"Repository: we-promise/sure Length of output: 41 🏁 Script executed: #!/bin/bash
# Search for controllers that might handle CSP reports
rg -n "def.*csp|def.*violation|def.*report" --type rb -i | grep -i "csp\|violation.*report"Repository: we-promise/sure Length of output: 85 🏁 Script executed: #!/bin/bash
# Look for any references to the endpoint path in controllers/routes
rg -n "csp.violation|csp_violation" --type rbRepository: we-promise/sure Length of output: 85 🏁 Script executed: #!/bin/bash
# Find routes.rb file
find . -name "routes.rb" -type fRepository: we-promise/sure Length of output: 77 🏁 Script executed: #!/bin/bash
# Search for controllers with CSP or violation handling
find . -path "*/app/controllers/*.rb" -type f | xargs rg -l "csp|violation" -iRepository: we-promise/sure Length of output: 43 🏁 Script executed: #!/bin/bash
# Broader search for any action that might handle /csp-violation-report
rg -n "csp.violation|csp_violation" -iRepository: we-promise/sure Length of output: 961 🏁 Script executed: #!/bin/bash
# Examine routes.rb for CSP report endpoint
cat ./config/routes.rbRepository: we-promise/sure Length of output: 16234 🏁 Script executed: #!/bin/bash
# Search routes.rb specifically for the endpoint
rg "csp|violation" ./config/routes.rb -iRepository: we-promise/sure Length of output: 41 Wire the CSP report endpoint before relying on report-only telemetry.
🤖 Prompt for AI Agents |
||
| end | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Nonces for inline scripts/styles managed by importmap and Hotwire | ||
| config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } | ||
| config.content_security_policy_nonce_directives = %w[script-src] | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| # REPORT-ONLY: violations are logged, nothing is blocked. | ||
| # Remove this line to enforce the policy. | ||
| config.content_security_policy_report_only = true | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,35 +2,52 @@ | |
|
|
||
| # CORS configuration for API access from mobile clients (Flutter) and other external apps. | ||
| # | ||
| # This enables Cross-Origin Resource Sharing for the /api, /oauth, and /sessions endpoints, | ||
| # allowing the Flutter mobile client and other authorized clients to communicate | ||
| # with the Rails backend. | ||
| # Allowed origins configured via ALLOWED_ORIGINS env var (comma-separated). | ||
| # Falls back to APP_DOMAIN if set, otherwise denies cross-origin requests. | ||
| # | ||
| # Examples: | ||
| # ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com | ||
| # APP_DOMAIN=app.example.com | ||
| # | ||
| # Security: wildcard origins (*) are intentionally not used. | ||
|
|
||
| def allowed_origins | ||
| if ENV["ALLOWED_ORIGINS"].present? | ||
| ENV["ALLOWED_ORIGINS"].split(",").map(&:strip) | ||
| elsif ENV["APP_DOMAIN"].present? | ||
| [ "https://#{ENV['APP_DOMAIN']}" ] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The APP_DOMAIN fallback always builds an Useful? React with 👍 / 👎. |
||
| else | ||
| Rails.logger.warn("[SECURITY] ALLOWED_ORIGINS and APP_DOMAIN not set — CORS will deny all cross-origin requests") | ||
| [] | ||
| end | ||
| end | ||
|
|
||
| Rails.application.config.middleware.insert_before 0, Rack::Cors do | ||
| allow do | ||
| # Allow requests from any origin for API endpoints | ||
| # Mobile apps and development environments need flexible CORS | ||
| origins "*" | ||
| origins(*allowed_origins) | ||
|
|
||
| # API endpoints for mobile client and third-party integrations | ||
| resource "/api/*", | ||
| headers: :any, | ||
| methods: %i[get post put patch delete options head], | ||
| expose: %w[X-Request-Id X-Runtime], | ||
| max_age: 86400, | ||
| credentials: true | ||
|
|
||
| resource "/oauth/token", | ||
| headers: :any, | ||
| methods: %i[post options], | ||
| max_age: 86400 | ||
|
|
||
| # OAuth endpoints for authentication flows | ||
| resource "/oauth/*", | ||
| resource "/oauth/revoke", | ||
| headers: :any, | ||
| methods: %i[get post put patch delete options head], | ||
| expose: %w[X-Request-Id X-Runtime], | ||
| methods: %i[post options], | ||
| max_age: 86400 | ||
|
|
||
| # Session endpoints for webview-based authentication | ||
| resource "/sessions/*", | ||
| headers: :any, | ||
| methods: %i[get post delete options head], | ||
| expose: %w[X-Request-Id X-Runtime], | ||
| max_age: 86400 | ||
| max_age: 86400, | ||
| credentials: true | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| Rails.application.configure do | ||
| Rack::MiniProfiler.config.skip_paths = [ "/design-system", "/assets", "/cable", "/manifest", "/favicon.ico", "/hotwire-livereload", "/logo-pwa.png" ] | ||
| Rack::MiniProfiler.config.max_traces_to_show = 50 | ||
| # rack-mini-profiler is only loaded in development | ||
| if defined?(Rack::MiniProfiler) | ||
| Rails.application.configure do | ||
| Rack::MiniProfiler.config.skip_paths = [ "/design-system", "/assets", "/cable", "/manifest", "/favicon.ico", "/hotwire-livereload", "/logo-pwa.png" ] | ||
| Rack::MiniProfiler.config.max_traces_to_show = 50 | ||
| end | ||
| end |
Uh oh!
There was an error while loading. Please reload this page.