Skip to content
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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

imtayadeway
Copy link
Contributor

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.

@rails-bot rails-bot bot added the actionpack label May 22, 2020
@kaspth kaspth requested a review from pixeltrix May 23, 2020 07:00
@p8
Copy link
Member

p8 commented Jun 15, 2020

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.

@imtayadeway
Copy link
Contributor Author

@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.
Copy link
Member

@jeremy jeremy left a 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*
Copy link
Member

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.

@pixeltrix
Copy link
Contributor

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?

@p8
Copy link
Member

p8 commented Jun 18, 2020

This is a pretty good article on downgrading CSP headers by including json endpoint in a frame.
https://lab.wallarm.com/how-to-trick-csp-in-letting-you-run-whatever-you-want-73cb5ff428aa/
Because the json is rendered by the browser as html you could add script tags for external JS files.

@pixeltrix
Copy link
Contributor

@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.

@pixeltrix
Copy link
Contributor

@jeremy a global non-HTML CSP may be a bit of a broad brush approach - the frame-ancestors directive applies to a range or embedded contexts, not just <frame> and <iframe>, e.g. <object> and <embed> would be affected as well. Put together with a default-src 'none' it could prevent embedded objects from working.

Maybe we could use a similar pattern to respond_to to control the CSP?, e.g.

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:

unless config.api_only
middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
middleware.use ::ActionDispatch::FeaturePolicy::Middleware
end

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?

@pixeltrix
Copy link
Contributor

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.

@jeremy
Copy link
Member

jeremy commented Jun 22, 2020

Maybe we could use a similar pattern to respond_to to control the CSP?, e.g.

Dig that! Particularly that existing policies remain the same, implying HTML, with format.whatever overrides following.

may be possible to move the CSP middleware higher up but that could affect the controller-level stuff so would need some investigation
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

+1 to move the middleware above any file service. That'd include AD::Static for folks serving assets and error pages from Rails.

does a similar problem exist with feature policies?

No idea! @imtayadeway @p8 do you know?

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

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

@imtayadeway
Copy link
Contributor Author

@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.

does a similar problem exist with feature policies?

I also have no idea, but I'll be sure to research this and get back to you.

@p8
Copy link
Member

p8 commented Jul 1, 2020

@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.

@rails-bot rails-bot bot added the railties label Jul 22, 2020
@imtayadeway
Copy link
Contributor Author

@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?

It may be possible to move the CSP middleware higher up but that could affect the controller-level stuff so would need some investigation.

This seemed to work, but is there anything I should check/may have missed?

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?

Would y'all be OK with my looking into this as a follow up?

@rails-bot
Copy link

rails-bot bot commented Oct 20, 2020

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.
Thank you for your contributions.

@rails-bot rails-bot bot added the stale label Oct 20, 2020
@imtayadeway
Copy link
Contributor Author

@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!

@rails-bot rails-bot bot removed the stale label Oct 21, 2020
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
Copy link
Member

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|
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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?

Base automatically changed from master to main January 14, 2021 17:01
@rails-bot
Copy link

rails-bot bot commented Apr 14, 2021

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.
Thank you for your contributions.

@rails-bot rails-bot bot added the stale label Apr 14, 2021
@rails-bot rails-bot bot closed this Apr 21, 2021
@jeremy
Copy link
Member

jeremy commented Sep 17, 2021

Keeping this alive. Still desirable, and necessary, since non-HTML content types may execute JavaScript.

@jeremy jeremy reopened this Sep 17, 2021
@rails-bot rails-bot bot removed the stale label Sep 17, 2021
@imtayadeway
Copy link
Contributor Author

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
Copy link
Contributor

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.

@jeremy
Copy link
Member

jeremy commented Sep 21, 2021

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

@tenderlove
Copy link
Member

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.

@imtayadeway
Copy link
Contributor Author

@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 💜

@tenderlove
Copy link
Member

@tenderlove I could pull 05344fc into a separate PR if you think that would suffice?

Ya, I think it would be great! Please do it 🙇🏻‍♀️

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

No need to apologize, thank you for the work you've done so far!! ❤️

@imtayadeway imtayadeway changed the title Generate content security policy for non-HTML responses Add new APIs to customize content security policy for non-HTML responses Mar 8, 2022
@imtayadeway
Copy link
Contributor Author

@tenderlove thanks! I pulled that out into #44635. Hoping to get back to this soon though and thank you for your continued patience!

@tenderlove
Copy link
Member

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants