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
Add new APIs to customize content security policy for non-HTML responses #39398
base: main
Are you sure you want to change the base?
Conversation
It's possible to downgrade CSP headers by loading non-html resources with less strict CSP headers. So I think this would be an improvement. |
@pixeltrix are you able to review at this time? No worries if not |
One feature of the content security policy DSL, though undocumented, is that it will not generate headers for non-HTML responses, even if a configuration is explicitly provided. While it may not seem obvious that anyone would want to send this header in an API response, Mozilla Observatory, for instance, recommends the following for API responses: `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'` (source: https://observatory.mozilla.org/faq/) The Secure Headers gem also makes recommendations about the content security policy for API responses: https://github.com/github/secure_headers#api-configurations As such, this removes the HTML guard clause from the `ContentSecurityPolicy` middleware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big +1 to making non-HTML CSP possible.
Think we should go further with this, even, and provide a strong default non-HTML CSP for all apps. This isn't something to fine-tune for each app; nobody should have to research & configure this when there's a clear default we can provide.
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
This is a great app-wide default that for all newly-generated apps, with a config/initializers/new_framework_defaults.rb opt-in for existing/upgrading apps.
We could do this with new top-level config, e.g.
Rails.application.config.api_content_security_policy do |policy|
# …
Rails.application.config.api_content_security_policy_report_only = false
Using "API" in this way may be misleading, though. It's really all non-HTML responses. But that's a bit of a mouthful:
Rails.application.config.non_html_content_security_policy do |policy|
In any case, we'd update the middleware to still check for HTML responses and use the main CSP with a fallback to the non-HTML CSP, if present, for other responses.
@@ -1,3 +1,6 @@ | |||
* Allow Content Security Policy DSL to generate for API responses. | |||
*Tim Wade* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's describe this a bit more. The summary in the pull request is great! We'll now send CSP headers with all responses, not just HTML, to protect API responses that could be loaded in a browser.
Sorry, it was a bad assumption on my part that it only applies to HTML requests but looking at the where the policy applies in the spec it doesn't mention API requests. We should definitely remove the limitation but wondering what the justification is for the Mozilla Observatory decision is? |
This is a pretty good article on downgrading CSP headers by including json endpoint in a frame. |
@p8 thanks for the link - according to that article we also need to be adding it to error pages as well so will need to give this some thought. |
@jeremy a global non-HTML CSP may be a bit of a broad brush approach - the Maybe we could use a similar pattern to Rails.application.config.content_security_policy do |format|
format.html 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
# The frame-ancestors directive obsoletes the X-Frame-Options header
policy.frame_ancestors :self
end
# Allow PDFs to be embedded in the page
format.pdf do |policy|
policy.default_src :self
policy.frame_ancestors :self
end
# Prevent any other type of request from being used in an
# embedded context, e.g. <frame>, <iframe>, <object> or <embed>
format.any do |policy|
policy.default_src :none
policy.frame_ancestors :none
end
end The yielded object would apply any of the directives to the html format to maintain backwards compatibility and wouldn't start adding CSPs to non-HTML requests unless updated to the new format. e.g. this: 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
end would be equivalent to this: Rails.application.config.content_security_policy do |format|
format.html 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
end
end To make developers upgrading aware of this change we can log a warning for requests in development mode where there isn't a CSP set and allow that to be switched off via a config option. Then there's the issue of the public exceptions middleware - it's higher up the stack than the CSP middleware so won't pickup anything from it. It may be possible to move the CSP middleware higher up but that could affect the controller-level stuff so would need some investigation. An easy alternative is a simple config option to the public exceptions middleware to add a CSP header. We'd also probably want some documentation in the security guide about adding a CSP header to static error pages delivered by Apache/nginx or a CDN such as custom error pages for AWS CloudFront. One other thing that'll need changing is this conditional so that API only apps load the CSP middleware: rails/railties/lib/rails/application/default_middleware_stack.rb Lines 69 to 72 in 664e949
That also reveals another question - does a similar problem exist with feature policies? Can loading an iframe without a feature policy allow bypassing of the policy on the parent browsing context? |
One other point - probably need to investigate whether this could affect Active Storage uploads as well since an attacker could upload a document or find an already uploaded document and load it into an iframe. |
Dig that! Particularly that existing policies remain the same, implying HTML, with
+1 to move the middleware above any file service. That'd include AD::Static for folks serving assets and error pages from Rails.
No idea! @imtayadeway @p8 do you know?
Great point. Think this is likely a separate concern, however, since AS blobs are served by redirect to external service URLs, putting them at arms' length from the app origin. /cc @georgeclaghorn |
@jeremy @pixeltrix thanks for your thoughtful responses. As it seems you've built a consensus around design, I'm happy to start work on this week.
I also have no idea, but I'll be sure to research this and get back to you. |
@jeremy I guess the same thing applies to feature policies but it adds to the CSP hack. If you can inject an iframe with a non-HTML response, and add a link to an external script, (ie what this PR would prevent), you could inject a script to request access to the camera, if the feature policy headers aren't defined for the non-HTML response. |
@jeremy @pixeltrix thanks for your patience while I've been working on this. I don't believe I'm quite there yet (I'll certainly need to update documentation, update new defaults), but wondered if I could get some feedback on what I have so far?
This seemed to work, but is there anything I should check/may have missed?
Would y'all be OK with my looking into this as a follow up? |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
@jeremy hi! I've been looking into updating documentation on my branch, and I'd like to do some more testing around the public exceptions middleware, but wondered if you had any feedback on how this is going at this stage? Thanks! |
headers[header_name(request)] = policy.build(context, nonce, nonce_directives) | ||
headers[header_name(request)] = policy.build(context, nonce, nonce_directives, headers[CONTENT_TYPE]) | ||
else | ||
if (Rails.env.development? || Rails.env.test?) && logger(request) && @warn_on_no_content_security_policy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checks for environment can't enter the action pack code. If we want different behavior per environment this should be a config on this class.
@directives[directive] = apply_mappings(sources) | ||
else | ||
@directives.delete(directive) | ||
(Mime::SET.map(&:to_sym) + [:any]).each do |type| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to generate methods for all the formats? I think we should only generate for the most common ones (html, js, json, css, any) and let the other formats be specified in another method like formats.any(:xml)
@@ -46,6 +46,8 @@ def build_stack | |||
middleware.use ::ActionDispatch::RequestId | |||
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies | |||
|
|||
middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware, config.warn_on_no_content_security_policy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are changing the position of this middleware to be earlier in the stack before the Rack::Logger. I don't think we should do that. What we want is to move before the public exceptions middleware so this should be probably after the Rack::Logger.
@@ -48,6 +46,10 @@ def header_name(request) | |||
def policy_present?(headers) | |||
headers[POLICY] || headers[POLICY_REPORT_ONLY] | |||
end | |||
|
|||
def logger(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for this method?
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Keeping this alive. Still desirable, and necessary, since non-HTML content types may execute JavaScript. |
Hi @jeremy ! Apologies - life really got in the way of this one. I'm happy to revisit this soon if it's still desirable |
content_security_policy only: :api do |format| | ||
format.json do |p| | ||
p.default_src :none | ||
p.frame_ancestors :none |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest including sandbox
in the default policy for API requests as well. It's a few more bytes and provides a little more protection against hypothetical situations.
Cool @imtayadeway! Think it's worth catching up with modern "strict CSP" as well (given that allowlist bypasses are common) since that could affect API design decisions here. /cc @pixeltrix |
As a short term solution could we just send the CSP for all responses? I'm not sure what the downsides are for just always sending a CSP. |
@tenderlove I could pull 05344fc into a separate PR if you think that would suffice? Apologies as I've not been able to get back to this any sooner, I've just been swamped with personal stuff. But looking to get this branch updated and responding to feedback soon 💜 |
Ya, I think it would be great! Please do it 🙇🏻♀️
No need to apologize, thank you for the work you've done so far!! ❤️ |
@tenderlove thanks! I pulled that out into #44635. Hoping to get back to this soon though and thank you for your continued patience! |
Great, thanks. I merged it! |
Summary
One feature of the content security policy DSL, though undocumented,
is that it will not generate headers for non-HTML responses, even if a
configuration is explicitly provided. While it may not seem obvious
that anyone would want to send this header in an API response, Mozilla
Observatory, for instance, recommends the following for API responses:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
(source: https://observatory.mozilla.org/faq/)
The Secure Headers gem also makes recommendations about the content
security policy for API responses: https://github.com/github/secure_headers#api-configurations
As such, this removes the HTML guard clause from the
ContentSecurityPolicy
middleware.Other Information
I imagine that it may be preferable to gate this behavior, assuming that the change is agreeable, but kindly request feedback before getting too deeply involved with that.