pax_global_header00006660000000000000000000000064140105554110014506gustar00rootroot0000000000000052 comment=5592e9ac79358ce5cfae2eb9b20228618aad3953 secure_headers-6.3.2/000077500000000000000000000000001401055541100144775ustar00rootroot00000000000000secure_headers-6.3.2/.github/000077500000000000000000000000001401055541100160375ustar00rootroot00000000000000secure_headers-6.3.2/.github/ISSUE_TEMPLATE.md000066400000000000000000000020531401055541100205440ustar00rootroot00000000000000# Feature Requests ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? --- # Bugs Console errors and deprecation warnings are considered bugs that should be addressed with more precise UA sniffing. Bugs caused by incorrect or invalid UA sniffing are also bugs. ### Expected outcome Describe what you expected to happen 1. I configure CSP to do X 1. When I inspect the response headers, the CSP should have included X ### Actual outcome 1. The generated policy did not include X ### Config Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`). ### Generated headers Provide a sample response containing the headers secure_headers-6.3.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000007001401055541100216350ustar00rootroot00000000000000## All PRs: * [ ] Has tests * [ ] Documentation updated ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? secure_headers-6.3.2/.github/workflows/000077500000000000000000000000001401055541100200745ustar00rootroot00000000000000secure_headers-6.3.2/.github/workflows/build.yml000066400000000000000000000010431401055541100217140ustar00rootroot00000000000000name: Build + Test on: [pull_request] jobs: build: name: Build + Test runs-on: ubuntu-latest strategy: matrix: ruby: [ '2.4', '2.5', '2.6', '2.7' ] steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby }} uses: actions/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake run: | gem install bundler bundle install --jobs 4 --retry 3 --without guard bundle exec rspec spec bundle exec rubocop secure_headers-6.3.2/.github/workflows/sync.yml000066400000000000000000000007631401055541100216010ustar00rootroot00000000000000# This workflow ensures the "master" branch is always up-to-date with the # "main" branch (our default one) name: sync_main_branch on: push: branches: [ main ] jobs: catch_up: runs-on: ubuntu-latest steps: - name: Check out the repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Merge development into master, then push it run: | git pull git checkout master git merge main git push secure_headers-6.3.2/.gitignore000066400000000000000000000001511401055541100164640ustar00rootroot00000000000000*.gem *.DS_STORE *.rbc .bundle .config .yardoc *.log Gemfile.lock _yardoc coverage pkg rdoc spec/reports secure_headers-6.3.2/.rspec000066400000000000000000000000521401055541100156110ustar00rootroot00000000000000--order rand --warnings --format progress secure_headers-6.3.2/.rubocop.yml000066400000000000000000000001251401055541100167470ustar00rootroot00000000000000inherit_gem: rubocop-github: - config/default.yml require: rubocop-performance secure_headers-6.3.2/.ruby-gemset000066400000000000000000000000161401055541100167400ustar00rootroot00000000000000secureheaders secure_headers-6.3.2/.ruby-version000066400000000000000000000000061401055541100171400ustar00rootroot000000000000002.6.6 secure_headers-6.3.2/CHANGELOG.md000066400000000000000000000560471401055541100163240ustar00rootroot00000000000000## 6.3.2 Add support for style-src-attr, style-src-elem, script-src-attr, and script-src-elem directives (@ggalmazor) ## 6.3.1 Fixes deprecation warnings when running under ruby 2.7 ## 6.3.0 Fixes newline injection issue ## 6.2.0 Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418 ## 6.1.2 Adds the ability to specify `SameSite=none` with the same configurability as `Strict`/`Lax` in order to disable Chrome's soon-to-be-lax-by-default state. ## 6.1.1 Adds the ability to disable the automatically-appended `'unsafe-inline'` value when nonces are used #404 (@will) ## 6.1 Adds support for navigate-to, prefetch-src, and require-sri-for #395 NOTE: this version is a breaking change due to the removal of HPKP. Remove the HPKP config, the standard is dead. Apologies for not doing a proper deprecate/major rev cycle :pray: ## 6.0 - See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes. ## 5.0.5 - A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x ## 5.0.4 - Adds support for `nonced_stylesheet_pack_tag` #373 (@paulfri) ## 5.0.3 - Add nonced versions of Rails link/include tags #372 (@steveh) ## 5.0.2 - Updates `Referrer-Policy` header to support multiple policy values ## 5.0.1 - Updates `Expect-CT` header to use a comma separator between directives, as specified in the most current spec. ## 5.0.0 Well this is a little embarassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide. ## 4.0.1 - Adds support for `worker-src` CSP directive to 4.x line (https://github.com/twitter/secureheaders/pull/364) ## 4.0 - See the [upgrading to 4.0](docs/upgrading-to-4-0.md) guide. Lots of breaking changes. ## 3.7.2 - Adds support for `worker-src` CSP directive to 3.x line (https://github.com/twitter/secureheaders/pull/364) ## 3.7.1 Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values. ## 3.7.0 Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322) ## 3.6.7 Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva! ## 3.6.5 Update clear-site-data header to use current format specified by the specification. ## 3.6.4 Fix case where mixing frame-src/child-src dynamically would behave in unexpected ways: https://github.com/twitter/secureheaders/pull/325 ## 3.6.3 Remove deprecation warning when setting `frame-src`. It is no longer deprecated. ## 3.6.2 Now that Safari 10 supports nonces and it appears to work, enable the nonce feature for safari. ## 3.6.1 Improved memory use via minor improvements clever hacks that are sadly needed. Thanks @carlosantoniodasilva! ## 3.6.0 Add support for the clear-site-data header ## 3.5.1 * Fix bug that can occur when useragent library version is older, resulting in a nil version sometimes. * Add constant for `strict-dynamic` ## 3.5.0 This release adds support for setting two CSP headers (enforced/report-only) and management around them. ## 3.4.1 Named Appends ### Small bugfix If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src). ### Named Appends Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. ```ruby def show if include_widget? @widget = widget.render use_content_security_policy_named_append(:widget_partial) end end SecureHeaders::Configuration.named_append(:widget_partial) do |request| if request.controller_instance.current_user.in_test_bucket? SecureHeaders.override_x_frame_options(request, "DENY") { child_src: %w(beta.thirdpartyhost.com) } else { child_src: %w(thirdpartyhost.com) } end end ``` You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: ```ruby SecureHeader::Configuration.default do |config| config.csp = { default_src: %w('self')} end SecureHeaders::Configuration.named_append(:A) do |request| { default_src: %w(myhost.com) } end SecureHeaders::Configuration.named_append(:B) do |request| { script_src: %w('unsafe-eval') } end ``` The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): ```ruby def index use_content_security_policy_named_append(:A) use_content_security_policy_named_append(:B) # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; end def show use_content_security_policy_named_append(:B) use_content_security_policy_named_append(:A) # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; end ``` ## 3.4.0 the frame-src/child-src transition for Firefox. Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here: ```ruby if supported_directives.include?(:child_src) @config[:child_src] = @config[:child_src] || @config[:frame_src] else @config[:frame_src] = @config[:frame_src] || @config[:child_src] end ``` Also, @koenpunt noticed that we were [loading view helpers](https://github.com/twitter/secureheaders/pull/272) in a way that Rails 5 did not like. ## 3.3.2 minor fix to silence warnings when using rake [@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants. ## 3.3.1 bugfix for boolean CSP directives [@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value. ## 3.3.0 referrer-policy support While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249). Additionally, two minor enhancements were added this version: 1. [Warn when the HPKP report host is the same as the current host](https://github.com/twitter/secureheaders/pull/246). By definition any generated reports would be reporting to a known compromised connection. 1. [Filter unsupported CSP directives when using Edge](https://github.com/twitter/secureheaders/pull/247). Previously, this was causing many warnings in the developer console. ## 3.2.0 Cookie settings and CSP hash sources ### Cookies SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. #### Boolean-based configuration Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. ```ruby config.cookies = { secure: true, # mark all cookies as Secure httponly: false, # do not mark any cookies as HttpOnly } ``` #### Hash-based configuration Hash-based configuration allows for fine-grained control. ```ruby config.cookies = { secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly } ``` #### SameSite cookie configuration SameSite cookies permit either `Strict` or `Lax` enforcement mode options. ```ruby config.cookies = { samesite: { strict: true # mark all cookies as SameSite=Strict } } ``` `Strict` and `Lax` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { samesite: { strict: { only: ['_rails_session'] }, lax: { only: ['_guest'] } } } ``` #### Hash `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. You can add hash sources directly to your policy : ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') # this is a made up value but browsers will show the expected hash in the console. script_src: %w(sha256-123456) } end ``` You can also use the automated inline script detection/collection/computation of hash source values in your app. ```bash rake secure_headers:generate_hashes ``` This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- scripts: app/views/asdfs/index.html.erb: - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" styles: app/views/asdfs/index.html.erb: - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` ##### Helpers **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. ```erb <%= hashed_style_tag do %> body { background-color: black; } <% end %> <%= hashed_style_tag do %> body { font-size: 30px; font-color: green; } <% end %> <%= hashed_javascript_tag do %> console.log(1) <% end %> ``` ``` Content-Security-Policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` ## 3.1.2 Bug fix for regression See https://github.com/twitter/secureheaders/pull/239 This meant that when header caches were regenerated upon calling `SecureHeaders.override(:name)` and using it with `use_secure_headers_override` would result in default values for anything other than CSP/HPKP. ## 3.1.1 Bug fix for regression See https://github.com/twitter/secureheaders/pull/235 `idempotent_additions?` would return false when comparing `OPT_OUT` with `OPT_OUT`, causing `header_hash_for` to return a header cache with `{ nil => nil }` which cause the middleware to blow up when `{ nil => nil }` was merged into the rack header hash. This is a regression in 3.1.0 only. Now it returns true. I've added a test case to ensure that `header_hash_for` will never return such an element. ## 3.1.0 Adding secure cookie support New feature: marking all cookies as secure. Added by @jmera in https://github.com/twitter/secureheaders/pull/231. In the future, we'll probably add the ability to whitelist individual cookies that should not be marked secure. PRs welcome. Internal refactoring: In https://github.com/twitter/secureheaders/pull/232, we changed the way dynamic CSP is handled internally. The biggest benefit is that highly dynamic policies (which can happen with multiple `append/override` calls per request) are handled better: 1. Only the CSP header cache is busted when using a dynamic policy. All other headers are preserved and don't need to be generated. Dynamic X-Frame-Options changes modify the cache directly. 1. Idempotency checks for policy modifications are deferred until the end of the request lifecycle and only happen once, instead of per `append/override` call. The idempotency check itself is fairly expensive itself. 1. CSP header string is produced at most once per request. ## 3.0.3 Bug fix for handling policy merges where appending a non-default source value (report-uri, plugin-types, frame-ancestors, base-uri, and form-action) would be combined with the default-src value. Appending a directive that doesn't exist in the current policy combines the new value with `default-src` to mimic the actual behavior of the addition. However, this does not make sense for non-default-src values (a.k.a. "fetch directives") and can lead to unexpected behavior like a `report-uri` value of `*`. Previously, this config: ``` { default_src => %w(*) } ``` When appending: ``` { report_uri => %w(https://report-uri.io/asdf) } ``` Would result in `default-src *; report-uri *` which doesn't make any sense at all. ## 3.0.2 Bug fix for handling CSP configs that supply a frozen hash. If a directive value is `nil`, then appending to a config with a frozen hash would cause an error since we're trying to modify a frozen hash. See https://github.com/twitter/secureheaders/pull/223. ## 3.0.1 Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (and Opera). See [the spec](https://www.w3.org/TR/upgrade-insecure-requests/) for details. ## 3.0.0 secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/main/docs/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. ## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning See https://github.com/twitter/secureheaders/issues/203 and https://github.com/twitter/secureheaders/commit/cfad0e52285353b88e46fe384e7cd60bf2a01735 >> Upon upgrading to secure_headers 2.5.0, I get a flood of these deprecations when running my tests: > [DEPRECATION] secure_header_options_for will not be supported in secure_headers /cc @bquorning ## 2.5.0 - 2016-01-06 22:11:02 UTC - 2.x deprecation warning release This release contains deprecation warnings for those wishing to upgrade to the 3.x series. With this release, fixing all deprecation warnings will make your configuration compatible when you decide to upgrade to the soon-to-be-released 3.x series (currently in pre-release stage). No changes to functionality should be observed unless you were using procs as CSP config values. ## 2.4.4 - 2015-12-03 23:29:42 UTC - Bug fix release If you use the `header_hash` method for setting your headers in middleware and you opted out of a header (via setting the value to `false`), you would run into an exception as described in https://github.com/twitter/secureheaders/pull/193 ``` NoMethodError: undefined method `name' for nil:NilClass # ./lib/secure_headers.rb:63:in `block in header_hash' # ./lib/secure_headers.rb:54:in `each' # ./lib/secure_headers.rb:54:in `inject' # ./lib/secure_headers.rb:54:in `header_hash' ``` ## 2.4.3 - 2015-10-23 18:35:43 UTC - Performance improvement @igrep reported an anti-patter in use regarding [UserAgentParser](https://github.com/ua-parser/uap-ruby). This caused UserAgentParser to reload it's entire configuration set *twice** per request. Moving this to a cached constant prevents the constant reinstantiation and will improve performance. https://github.com/twitter/secureheaders/issues/187 ## 2.4.2 - 2015-10-20 20:22:08 UTC - Bug fix release A nasty regression meant that many CSP configuration values were "reset" after the first request, one of these being the "enforce" flag. See https://github.com/twitter/secureheaders/pull/184 for the full list of fields that were affected. Thanks to @spdawson for reporting this https://github.com/twitter/secureheaders/issues/183 ## 2.4.1 - 2015-10-14 22:57:41 UTC - More UA sniffing This release may change the output of headers based on per browser support. Unsupported directives will be omitted based on the user agent per request. See https://github.com/twitter/secureheaders/pull/179 p.s. this will likely be the last non-bugfix release for the 2.x line. 3.x will be a major change. Sneak preview: https://github.com/twitter/secureheaders/pull/181 ## 2.4.0 - 2015-10-01 23:05:38 UTC - Some internal changes affecting behavior, but not functionality If you leveraged `secure_headers` automatic filling of empty directives, the header value will change but it should not affect how the browser applies the policy. The content of CSP reports may change if you do not update your policy. before === ```ruby config.csp = { :default_src => "'self'" } ``` would produce `default-src 'self'; connect-src 'self'; frame-src 'self' ... etc.` after === ```ruby config.csp = { :default_src => "'self'" } ``` will produce `default-src 'self'` The reason for this is that a `default-src` violation was basically impossible to handle. Chrome sends an `effective-directive` which helps indicate what kind of violation occurred even if it fell back to `default-src`. This is part of the [CSP Level 2 spec](http://www.w3.org/TR/CSP2/#violation-report-effective-directive) so hopefully other browsers will implement this soon. Workaround === Just set the values yourself, but really a `default-src` of anything other than `'none'` implies the policy can be tightened dramatically. "ZOMG don't you work for github and doesn't github send a `default-src` of `*`???" Yes, this is true. I disagree with this but at the same time, github defines every single known directive that a browser supports so `default-src` will only apply if a new directive is introduced, and we'd rather fail open. For now. ```ruby config.csp = { :default_src => "'self'", :connect_src => "'self'", :frame_src => "'self'" ... etc. } ``` Besides, relying on `default-src` is often not what you want and encourages an overly permissive policy. I've seen it. Seriously. `default-src 'unsafe-inline' 'unsafe-eval' https: http:;` That's terrible. ## 2.3.0 - 2015-09-30 19:43:09 UTC - Add header_hash feature for use in middleware. See https://github.com/twitter/secureheaders/issues/167 and https://github.com/twitter/secureheaders/pull/168 tl;dr is that there is a class method `SecureHeaders::header_hash` that will return a hash of header name => value pairs useful for merging with the rack header hash in middleware. ## 2.2.4 - 2015-08-26 23:31:37 UTC - Print deprecation warning for 1.8.7 users As discussed in https://github.com/twitter/secureheaders/issues/154 ## 2.2.3 - 2015-08-14 20:26:12 UTC - Adds ability to opt-out of automatically adding data: sources to img-src See https://github.com/twitter/secureheaders/pull/161 ## 2.2.2 - 2015-07-02 21:18:38 UTC - Another option for config granularity. See https://github.com/twitter/secureheaders/pull/147 Allows you to override a controller method that returns a config in the context of the executing action. ## 2.2.1 - 2015-06-24 21:01:57 UTC - When using nonces, do not include the nonce for safari / IE See https://github.com/twitter/secureheaders/pull/150 Safari will generate a warning that it doesn't support nonces. Safari will fall back to the `unsafe-inline`. Things will still work, but an ugly message is printed to the console. This opts out safari and IE users from the inline script protection. I haven't verified any IE behavior yet, so I'm just assuming it doesn't work. ## 2.2.0 - 2015-06-18 22:01:23 UTC - Pass controller reference to callable config value expressions. https://github.com/twitter/secureheaders/pull/148 Facilitates better per-request config: `:enforce => lambda { |controller| controller.current_user.beta_testing? }` **NOTE** if you used `lambda` config values, this will raise an exception until you add the controller reference: bad: `lambda { true }` good: `lambda { |controller| true }` `proc { true }` `proc { |controller| true }` ## v2.1.0 - 2015-05-07 18:34:56 UTC - Add hpkp support Includes https://github.com/twitter/secureheaders/pull/143 (which is really just https://github.com/twitter/secureheaders/pull/132) from @thirstscolr ## v2.0.2 - 2015-05-05 03:09:44 UTC - Add report_uri constant value Just a small change that adds a constant that was missing as reported in https://github.com/twitter/secureheaders/issues/141 ## v2.0.1 - 2015-03-20 18:46:47 UTC - View Helpers Fixed Fixes an issue where view helpers (for nonces, hashes, etc) weren't available in views. ## 2.0.0 - 2015-01-23 20:23:56 UTC - 2.0 This release contains support for more csp level 2 features such as the new directives, the script hash integration, and more. It also sets a new header by default: `X-Permitted-Cross-Domain-Policies` Support for hpkp is not included in this release as the implementations are still very unstable. :rocket: ## v.2.0.0.pre2 - 2014-12-06 01:55:42 UTC - Adds X-Permitted-Cross-Domain-Policies support by default The only change between this and the first pre release is that the X-Permitted-Cross-Domain-Policies support is included. ## v1.4.0 - 2014-12-06 01:54:48 UTC - Deprecate features in preparation for 2.0 This removes the forwarder and "experimental" feature. The forwarder wasn't well maintained and created a lot of headaches. Also, it was using an outdated certificate pack for compatibility. That's bad. The experimental feature wasn't really used and it complicated the codebase a lot. It's also a questionably useful API that is very confusing. ## v2.0.0.pre - 2014-11-14 00:54:07 UTC - 2.0.0.pre - CSP level 2 support This release is intended to be ready for CSP level 2. Mainly, this means there is direct support for hash/nonce of inline content and includes many new directives (which do not inherit from default-src) ## v1.3.4 - 2014-10-13 22:05:44 UTC - * Adds X-Download-Options support * Adds support for X-XSS-Protection reporting * Defers loading of rails engine for faster boot times ## v1.3.3 - 2014-08-15 02:30:24 UTC - hsts preload confirmation value support @agl just made a new option for HSTS representing confirmation that a site wants to be included in a browser's preload list (https://hstspreload.appspot.com). This just adds a new 'preload' option to the HSTS settings to specify that option. ## v1.3.2 - 2014-08-14 00:01:32 UTC - Add app tagging support Tagging Requests It's often valuable to send extra information in the report uri that is not available in the reports themselves. Namely, "was the policy enforced" and "where did the report come from" ```ruby { :tag_report_uri => true, :enforce => true, :app_name => 'twitter', :report_uri => 'csp_reports' } ``` Results in ``` report-uri csp_reports?enforce=true&app_name=twitter ``` secure_headers-6.3.2/CODE_OF_CONDUCT.md000066400000000000000000000062241401055541100173020ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neil.matatall@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ secure_headers-6.3.2/CONTRIBUTING.md000066400000000000000000000035361401055541100167370ustar00rootroot00000000000000## Contributing [fork]: https://github.com/twitter/secureheaders/fork [pr]: https://github.com/twitter/secureheaders/compare [style]: https://github.com/styleguide/ruby [code-of-conduct]: CODE_OF_CONDUCT.md Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Submitting a pull request 0. [Fork][fork] and clone the repository 0. Configure and install the dependencies: `bundle install` 0. Make sure the tests pass on your machine: `bundle exec rspec spec` 0. Create a new branch: `git checkout -b my-branch-name` 0. Make your change, add tests, and make sure the tests still pass and that no warnings are raised 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Write tests. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). ## Releasing 0. Ensure CI is green 0. Pull the latest code 0. Increment the version 0. Run `gem build secure_headers.gemspec` 0. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem 0. Test behavior locally, branch deploy, whatever needs to happen 0. Run `bundle exec rake release` ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) secure_headers-6.3.2/Gemfile000066400000000000000000000006221401055541100157720ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" gemspec group :test do gem "coveralls" gem "json" gem "pry-nav" gem "rack" gem "rspec" gem "rubocop", "< 0.68" gem "rubocop-github" gem "rubocop-performance" gem "term-ansicolor" gem "tins" end group :guard do gem "growl" gem "guard-rspec", platforms: [:ruby] gem "rb-fsevent" gem "terminal-notifier-guard" end secure_headers-6.3.2/Guardfile000066400000000000000000000006221401055541100163240ustar00rootroot00000000000000# frozen_string_literal: true guard :rspec, cmd: "bundle exec rspec", all_on_start: true, all_after_pass: true do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # RSpec files rspec = dsl.rspec watch(rspec.spec_helper) { rspec.spec_dir } watch(rspec.spec_support) { rspec.spec_dir } watch(rspec.spec_files) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } end secure_headers-6.3.2/LICENSE000066400000000000000000000021071401055541100155040ustar00rootroot00000000000000Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Twitter, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. secure_headers-6.3.2/README.md000066400000000000000000000212431401055541100157600ustar00rootroot00000000000000# Secure Headers ![Build + Test](https://github.com/github/secure_headers/workflows/Build%20+%20Test/badge.svg?branch=main) **main branch represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - https://csp.withgoogle.com - https://csp.withgoogle.com/docs/strict-csp.html - https://csp-evaluator.withgoogle.com - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html) - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/) - Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/). - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/). It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes. This is on default but can be turned off by using `config.cookies = SecureHeaders::OPT_OUT`. `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. ## Documentation - [Named overrides and appends](docs/named_overrides_and_appends.md) - [Per action configuration](docs/per_action_configuration.md) - [Cookies](docs/cookies.md) - [Hashes](docs/hashes.md) - [Sinatra Config](docs/sinatra.md) ## Configuration If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. **Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements. ```ruby SecureHeaders::Configuration.default do |config| config.cookies = { secure: true, # mark all cookies as "Secure" httponly: true, # mark all cookies as "HttpOnly" samesite: { lax: true # mark all cookies as SameSite=lax } } # Add "; preload" and submit the site to hstspreload.org for best protection. config.hsts = "max-age=#{1.week.to_i}" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.referrer_policy = %w(origin-when-cross-origin strict-origin-when-cross-origin) config.csp = { # "meta" values. these will shape the header, but the values are not included in the header. preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. disable_nonce_backwards_compatibility: true, # default: false. If false, `unsafe-inline` will be added automatically when using nonces. If true, it won't. See #403 for why you'd want this. # directive values: these values will directly translate into source directives default_src: %w('none'), base_uri: %w('self'), block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), form_action: %w('self' github.com), frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), sandbox: true, # true and [] will set a maximally restrictive setting plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), script_src_elem: %w('self'), script_src_attr: %w('self'), style_src: %w('unsafe-inline'), style_src_elem: %w('unsafe-inline'), style_src_attr: %w('unsafe-inline'), worker_src: %w('self'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. config.csp_report_only = config.csp.merge({ img_src: %w(somewhereelse.com), report_uri: %w(https://report-uri.io/example-csp-report-only) }) end ``` ## Default values All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is: ``` Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' Strict-Transport-Security: max-age=631138519 X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: sameorigin X-Permitted-Cross-Domain-Policies: none X-Xss-Protection: 1; mode=block ``` ## API configurations Which headers you decide to use for API responses is entirely a personal choice. Things like X-Frame-Options seem to have no place in an API response and would be wasting bytes. While this is true, browsers can do funky things with non-html responses. At the minimum, we suggest CSP: ```ruby SecureHeaders::Configuration.override(:api) do |config| config.csp = { default_src: 'none' } config.hsts = SecureHeaders::OPT_OUT config.x_frame_options = SecureHeaders::OPT_OUT config.x_content_type_options = SecureHeaders::OPT_OUT config.x_xss_protection = SecureHeaders::OPT_OUT config.x_permitted_cross_domain_policies = SecureHeaders::OPT_OUT end ``` However, I would consider these headers anyways depending on your load and bandwidth requirements. ## Acknowledgements This project originated within the Security team at Twitter. An archived fork from the point of transition is here: https://github.com/twitter-archive/secure_headers. Contributors include: * Neil Matatall @oreoshake * Chris Aniszczyk * Artur Dryomov * Bjørn Mæland * Arthur Chiu * Jonathan Viney * Jeffrey Horn * David Collazo * Brendon Murphy * William Makley * Reed Loden * Noah Kantrowitz * Wyatt Anderson * Salimane Adjao Moustapha * Francois Chagnon * Jeff Hodges * Ian Melven * Darío Javier Cravero * Logan Hasson * Raul E Rangel * Steve Agalloco * Nate Collings * Josh Kalderimis * Alex Kwiatkowski * Julich Mera * Jesse Storimer * Tom Daniels * Kolja Dummann * Jean-Philippe Doyle * Blake Hitchcock * vanderhoorn * orthographic-pedant * Narsimham Chelluri If you've made a contribution and see your name missing from the list, make a PR and add it! ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) * Node.js (express) [helmet](https://github.com/helmetjs/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/) * PHP [secure-headers](https://github.com/BePsvPT/secure-headers) secure_headers-6.3.2/Rakefile000066400000000000000000000012061401055541100161430ustar00rootroot00000000000000#!/usr/bin/env rake # frozen_string_literal: true require "bundler/gem_tasks" require "rspec/core/rake_task" require "net/http" require "net/https" RSpec::Core::RakeTask.new begin require "rdoc/task" rescue LoadError require "rdoc/rdoc" require "rake/rdoctask" RDoc::Task = Rake::RDocTask end begin require "rubocop/rake_task" RuboCop::RakeTask.new rescue LoadError task(:rubocop) { $stderr.puts "RuboCop is disabled" } end RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = "rdoc" rdoc.title = "SecureHeaders" rdoc.options << "--line-numbers" rdoc.rdoc_files.include("lib/**/*.rb") end task default: [:spec, :rubocop] secure_headers-6.3.2/docs/000077500000000000000000000000001401055541100154275ustar00rootroot00000000000000secure_headers-6.3.2/docs/cookies.md000066400000000000000000000034731401055541100174140ustar00rootroot00000000000000## Cookies SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. #### Defaults By default, all cookies will get both `Secure`, `HttpOnly`, and `SameSite=Lax`. ```ruby config.cookies = { secure: true, # defaults to true but will be a no op on non-HTTPS requests httponly: true, # defaults to true samesite: { # defaults to set `SameSite=Lax` lax: true } } ``` #### Boolean-based configuration Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. *Note: As of 4.0, you must use OPT_OUT rather than false to opt out of the defaults.* ```ruby config.cookies = { secure: true, # mark all cookies as Secure httponly: SecureHeaders::OPT_OUT, # do not mark any cookies as HttpOnly } ``` #### Hash-based configuration Hash-based configuration allows for fine-grained control. ```ruby config.cookies = { secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly } ``` #### SameSite cookie configuration SameSite cookies permit either `Strict` or `Lax` enforcement mode options. ```ruby config.cookies = { samesite: { strict: true # mark all cookies as SameSite=Strict } } ``` `Strict`, `Lax`, and `None` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { samesite: { strict: { only: ['session_id_duplicate'] }, lax: { only: ['_guest', '_rails_session', 'device_id'] }, none: { only: ['_tracking', 'saml_cookie', 'session_id'] }, } } ``` secure_headers-6.3.2/docs/hashes.md000066400000000000000000000044441401055541100172320ustar00rootroot00000000000000## Hash `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. You can add hash sources directly to your policy : ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') # this is a made up value but browsers will show the expected hash in the console. script_src: %w(sha256-123456) } end ``` You can also use the automated inline script detection/collection/computation of hash source values in your app. ```bash rake secure_headers:generate_hashes ``` This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- scripts: app/views/asdfs/index.html.erb: - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" styles: app/views/asdfs/index.html.erb: - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` ##### Helpers **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. ```erb <%= hashed_style_tag do %> body { background-color: black; } <% end %> <%= hashed_style_tag do %> body { font-size: 30px; font-color: green; } <% end %> <%= hashed_javascript_tag do %> console.log(1) <% end %> ``` ``` Content-Security-Policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` secure_headers-6.3.2/docs/named_overrides_and_appends.md000066400000000000000000000070031401055541100234530ustar00rootroot00000000000000## Named Appends Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. Reusing a configuration name is not allowed and will throw an exception. ```ruby def show if include_widget? @widget = widget.render use_content_security_policy_named_append(:widget_partial) end end SecureHeaders::Configuration.named_append(:widget_partial) do |request| SecureHeaders.override_x_frame_options(request, "DENY") if request.controller_instance.current_user.in_test_bucket? { child_src: %w(beta.thirdpartyhost.com) } else { child_src: %w(thirdpartyhost.com) } end end ``` You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: ```ruby SecureHeader::Configuration.default do |config| config.csp = { default_src: %w('self')} end SecureHeaders::Configuration.named_append(:A) do |request| { default_src: %w(myhost.com) } end SecureHeaders::Configuration.named_append(:B) do |request| { script_src: %w('unsafe-eval') } end ``` The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): ```ruby def index use_content_security_policy_named_append(:A) use_content_security_policy_named_append(:B) # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; end def show use_content_security_policy_named_append(:B) use_content_security_policy_named_append(:A) # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; end ``` ## Named overrides Named overrides serve two purposes: * To be able to refer to a configuration by simple name. * By precomputing the headers for a named configuration, the headers generated once and reused over every request. To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override. ```ruby class ApplicationController < ActionController::Base SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(example.org) } end # override default configuration SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| config.csp[:script_src] << "otherdomain.com" end end class MyController < ApplicationController def index # Produces default-src 'self'; script-src example.org otherdomain.com use_secure_headers_override(:script_from_otherdomain_com) end def show # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com use_secure_headers_override(:another_config) end end ``` Reusing a configuration name is not allowed and will throw an exception. By default, a no-op configuration is provided. No headers will be set when this default override is used. ```ruby class MyController < ApplicationController def index SecureHeaders.opt_out_of_all_protection(request) end end ``` secure_headers-6.3.2/docs/per_action_configuration.md000066400000000000000000000111141401055541100230210ustar00rootroot00000000000000## Per-action configuration You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request. ```ruby # Given a config of: ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end class MyController < ApplicationController def index # Append value to the source list, override 'none' values # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com)) # Overrides the previously set source list, override 'none' values # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self' override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self')) # Global settings default to "sameorigin" override_x_frame_options("DENY") end ``` The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. * `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` ## Appending / overriding Content Security Policy When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`. #### Append to the policy with a directive other than `default_src` The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') } end ``` Code | Result ------------- | ------------- `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` #### Nonce You can use a view helper to automatically add nonces to script tags: ```erb <%= nonced_javascript_tag do %> console.log("nonced!"); <% end %> <%= nonced_style_tag do %> body { background-color: black; } <% end %> <%= nonced_javascript_include_tag "include.js" %> <%= nonced_javascript_pack_tag "pack.js" %> <%= nonced_stylesheet_link_tag "link.css" %> <%= nonced_stylesheet_pack_tag "pack.css" %> ``` becomes: ```html ``` ``` Content-Security-Policy: ... script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; ``` `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags. ```erb ``` ## Clearing browser cache You can clear the browser cache after the logout request by using the following. ``` ruby class ApplicationController < ActionController::Base # Configuration override to send the Clear-Site-Data header. SecureHeaders::Configuration.override(:clear_browser_cache) do |config| config.clear_site_data = [ SecureHeaders::ClearSiteData::ALL_TYPES ] end # Clears the browser's cache for browsers supporting the Clear-Site-Data # header. # # Returns nothing. def clear_browser_cache SecureHeaders.use_secure_headers_override(request, :clear_browser_cache) end end class SessionsController < ApplicationController after_action :clear_browser_cache, only: :destroy end ``` secure_headers-6.3.2/docs/sinatra.md000066400000000000000000000006601401055541100174140ustar00rootroot00000000000000## Sinatra Here's an example using SecureHeaders for Sinatra applications: ```ruby require 'rubygems' require 'sinatra' require 'haml' require 'secure_headers' use SecureHeaders::Middleware SecureHeaders::Configuration.default do |config| ... end class Donkey < Sinatra::Application set :root, APP_ROOT get '/' do SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT) haml :index end end ``` secure_headers-6.3.2/docs/upgrading-to-3-0.md000066400000000000000000000164151401055541100206550ustar00rootroot00000000000000`secure_headers` 3.0 is a near-complete rewrite. It includes breaking changes and removes a lot of features that were either leftover from the days when the CSP standard was not fully adopted or were just downright confusing. Changes == | What | < = 2.x | >= 3.0 | | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` | | nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). | | `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | | `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | | Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | | Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior | | Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` | Migrating to 3.x from <= 2.x == 1. Convert all headers except for CSP/HPKP using hashes to string values. The values are validated at runtime and will provide guidance on misconfigured headers. 1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table. 1. Convert all CSP space-delimited directives to an array of strings. 1. Convert all `enforce: true|false` to `report_only: true|false`. 1. Remove `ensure_security_headers` from controllers (3.x uses a middleware instead). Everything is terrible, why should I upgrade? == `secure_headers` <= 2.x built every header per request using a series of automatically included `before_filters`. This is horribly inefficient because: 1. `before_filters` are slow and adding 8 per request isn't great 1. We are rebuilding strings that may never change for every request 1. Errors in the request may mean that the headers never get set in the first place `secure_headers` 3.x sets headers in rack middleware that runs once per request and uses configuration values passed via `request.env`. This is much more efficient and somewhat guarantees that headers will always be set. **The values for the headers are cached and reused per request**. Also, there is a more flexible API for customizing content security policies / X-Frame-Options. In practice, none of the other headers need granular controls. One way of customizing headers per request is to use the helper methods. The only downside of this technique is that headers will be computed from scratch. See the [README](README.md) for more information. secure_headers-6.3.2/docs/upgrading-to-4-0.md000066400000000000000000000026411401055541100206520ustar00rootroot00000000000000## script_src must be set Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose. However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this. ## Default Content Security Policy The default CSP has changed to be more universal without sacrificing too much security. * Flash/Java disabled by default * `img-src` allows data: images and favicons (among others) * `style-src` allows inline CSS by default (most find it impossible/impractical to remove inline content today) * `form-action` (not governed by `default-src`, practically treated as `*`) is set to `'self'` Previously, the default CSP was: `Content-Security-Policy: default-src 'self'` The new default policy is: `default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:` ## CSP configuration * Setting `report_only: true` in a CSP config will raise an error. Instead, set `csp_report_only`. * Setting `frame_src` and `child_src` when values don't match will raise an error. Just use `frame_src`. ## config.secure_cookies removed Use `config.cookies` instead. ## Supported ruby versions We've dropped support for ruby versions <= 2.2. Sorry. secure_headers-6.3.2/docs/upgrading-to-5-0.md000066400000000000000000000010511401055541100206450ustar00rootroot00000000000000## All cookies default to secure/httponly/SameSite=Lax By default, *all* cookies will be marked as `SameSite=lax`,`secure`, and `httponly`. To opt-out, supply `SecureHeaders::OPT_OUT` as the value for `SecureHeaders.cookies` or the individual configs. Setting these values to `false` will raise an error. ```ruby # specific opt outs config.cookies = { secure: SecureHeaders::OPT_OUT, httponly: SecureHeaders::OPT_OUT, samesite: SecureHeaders::OPT_OUT, } # nuclear option, just make things work again config.cookies = SecureHeaders::OPT_OUT ``` secure_headers-6.3.2/docs/upgrading-to-6-0.md000066400000000000000000000075741401055541100206660ustar00rootroot00000000000000## Named overrides are now dynamically applied The original implementation of name overrides worked by making a copy of the default policy, applying the overrides, and storing the result for later use. But, this lead to unexpected results if named overrides were combined with a dynamic policy change. If a change was made to the default configuration during a request, followed by a named override, the dynamic changes would be lost. To keep things consistent named overrides have been rewritten to work the same as named appends in that they always operate on the configuration for the current request. As an example: ```ruby class ApplicationController < ActionController::Base Configuration.default do |config| config.x_frame_options = SecureHeaders::OPT_OUT end SecureHeaders::Configuration.override(:dynamic_override) do |config| config.x_content_type_options = "nosniff" end end class FooController < ApplicationController def bar # Dynamically update the default config for this request override_x_frame_options("DENY") append_content_security_policy_directives(frame_src: "3rdpartyprovider.com") # Override everything, discard modifications above use_secure_headers_override(:dynamic_override) end end ``` Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` set to `nosniff`. ## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge` These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like Ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`. ## `Configuration#get` has been removed This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense. ## Configuration headers are no longer cached Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely. ## Calling the default configuration more than once will result in an Exception Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once. ## All user agent sniffing has been removed The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`. The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results in confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together). secure_headers-6.3.2/lib/000077500000000000000000000000001401055541100152455ustar00rootroot00000000000000secure_headers-6.3.2/lib/secure_headers.rb000066400000000000000000000222071401055541100205560ustar00rootroot00000000000000# frozen_string_literal: true require "secure_headers/hash_helper" require "secure_headers/headers/cookie" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" require "secure_headers/headers/strict_transport_security" require "secure_headers/headers/x_xss_protection" require "secure_headers/headers/x_content_type_options" require "secure_headers/headers/x_download_options" require "secure_headers/headers/x_permitted_cross_domain_policies" require "secure_headers/headers/referrer_policy" require "secure_headers/headers/clear_site_data" require "secure_headers/headers/expect_certificate_transparency" require "secure_headers/middleware" require "secure_headers/railtie" require "secure_headers/view_helper" require "singleton" require "secure_headers/configuration" # Provide SecureHeaders::OPT_OUT as a config value to disable a given header module SecureHeaders class NoOpHeaderConfig include Singleton def boom(*args) raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead." end def to_h {} end def dup self.class.instance end def opt_out? true end alias_method :[], :boom alias_method :[]=, :boom alias_method :keys, :boom end OPT_OUT = NoOpHeaderConfig.instance SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze CSP = ContentSecurityPolicy class << self # Public: override a given set of directives for the current request. If a # value already exists for a given directive, it will be overridden. # # If CSP was previously OPT_OUT, a new blank policy is used. # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def override_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) if [:both, :enforced].include?(target) if config.csp.opt_out? config.csp = ContentSecurityPolicyConfig.new({}) end config.csp.merge!(additions) end if [:both, :report_only].include?(target) if config.csp_report_only.opt_out? config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) end config.csp_report_only.merge!(additions) end override_secure_headers_request_config(request, config) end # Public: appends source values to the current configuration. If no value # is set for a given directive, the value will be merged with the default-src # value. If a value exists for the given directive, the values will be combined. # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) if [:both, :enforced].include?(target) && !config.csp.opt_out? config.csp.append(additions) end if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out? config.csp_report_only.append(additions) end override_secure_headers_request_config(request, config) end def use_content_security_policy_named_append(request, name) additions = SecureHeaders::Configuration.named_appends(name).call(request) append_content_security_policy_directives(request, additions) end # Public: override X-Frame-Options settings for this request. # # value - deny, sameorigin, or allowall # # Returns the current config def override_x_frame_options(request, value) config = config_for(request) config.update_x_frame_options(value) override_secure_headers_request_config(request, config) end # Public: opts out of setting a given header by creating a temporary config # and setting the given headers config to OPT_OUT. def opt_out_of_header(request, header_key) config = config_for(request) config.opt_out(header_key) override_secure_headers_request_config(request, config) end # Public: opts out of setting all headers by telling secure_headers to use # the NOOP configuration. def opt_out_of_all_protection(request) use_secure_headers_override(request, Configuration::NOOP_OVERRIDE) end # Public: Builds the hash of headers that should be applied base on the # request. # # StrictTransportSecurity is not applied to http requests. # See #config_for to determine which config is used for a given request. # # Returns a hash of header names => header values. The value # returned is meant to be merged into the header value from `@app.call(env)` # in Rack middleware. def header_hash_for(request) prevent_dup = true config = config_for(request, prevent_dup) config.validate_config! headers = config.generate_headers if request.scheme != HTTPS headers.delete(StrictTransportSecurity::HEADER_NAME) end headers end # Public: specify which named override will be used for this request. # Raises an argument error if no named override exists. # # name - the name of the previously configured override. def use_secure_headers_override(request, name) config = config_for(request) config.override(name) override_secure_headers_request_config(request, config) end # Public: gets or creates a nonce for CSP. # # The nonce will be added to script_src # # Returns the nonce def content_security_policy_script_nonce(request) content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC) end # Public: gets or creates a nonce for CSP. # # The nonce will be added to style_src # # Returns the nonce def content_security_policy_style_nonce(request) content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC) end # Public: Retreives the config for a given header type: # # Checks to see if there is an override for this request, then # Checks to see if a named override is used for this request, then # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || Configuration.send(:default_config) # Global configs are frozen, per-request configs are not. When we're not # making modifications to the config, prevent_dup ensures we don't dup # the object unnecessarily. It's not necessarily frozen to begin with. if config.frozen? && !prevent_dup config.dup else config end end private TARGETS = [:both, :enforced, :report_only] def raise_on_unknown_target(target) unless TARGETS.include?(target) raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]" end end def config_and_target(request, target) config = config_for(request) target = guess_target(config) unless target raise_on_unknown_target(target) [config, target] end def guess_target(config) if !config.csp.opt_out? && !config.csp_report_only.opt_out? :both elsif !config.csp.opt_out? :enforced elsif !config.csp_report_only.opt_out? :report_only else :both end end # Private: gets or creates a nonce for CSP. # # Returns the nonce def content_security_policy_nonce(request, script_or_style) request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY]) request.env[NONCE_KEY] end # Private: convenience method for specifying which configuration object should # be used for this request. # # Returns the config. def override_secure_headers_request_config(request, config) request.env[SECURE_HEADERS_CONFIG] = config end end # These methods are mixed into controllers and delegate to the class method # with the same name. def use_secure_headers_override(name) SecureHeaders.use_secure_headers_override(request, name) end def content_security_policy_script_nonce SecureHeaders.content_security_policy_script_nonce(request) end def content_security_policy_style_nonce SecureHeaders.content_security_policy_style_nonce(request) end def opt_out_of_header(header_key) SecureHeaders.opt_out_of_header(request, header_key) end def append_content_security_policy_directives(additions) SecureHeaders.append_content_security_policy_directives(request, additions) end def override_content_security_policy_directives(additions) SecureHeaders.override_content_security_policy_directives(request, additions) end def override_x_frame_options(value) SecureHeaders.override_x_frame_options(request, value) end def use_content_security_policy_named_append(name) SecureHeaders.use_content_security_policy_named_append(request, name) end end secure_headers-6.3.2/lib/secure_headers/000077500000000000000000000000001401055541100202265ustar00rootroot00000000000000secure_headers-6.3.2/lib/secure_headers/configuration.rb000066400000000000000000000216311401055541100234250ustar00rootroot00000000000000# frozen_string_literal: true require "yaml" module SecureHeaders class Configuration DEFAULT_CONFIG = :default NOOP_OVERRIDE = "secure_headers_noop_override" class AlreadyConfiguredError < StandardError; end class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self # Public: Set the global default configuration. # # Optionally supply a block to override the defaults set by this library. # # Returns the newly created config. def default(&block) if defined?(@default_config) raise AlreadyConfiguredError, "Policy already configured" end # Define a built-in override that clears all configuration options and # results in no security headers being set. override(NOOP_OVERRIDE) do |config| CONFIG_ATTRIBUTES.each do |attr| config.instance_variable_set("@#{attr}", OPT_OUT) end end new_config = new(&block).freeze new_config.validate_config! @default_config = new_config end alias_method :configure, :default # Public: create a named configuration that overrides the default config. # # name - use an idenfier for the override config. # base - override another existing config, or override the default config # if no value is supplied. # # Returns: the newly created config def override(name, &block) @overrides ||= {} raise "Provide a configuration block" unless block_given? if named_append_or_override_exists?(name) raise AlreadyConfiguredError, "Configuration already exists" end @overrides[name] = block end def overrides(name) @overrides ||= {} @overrides[name] end def named_appends(name) @appends ||= {} @appends[name] end def named_append(name, &block) @appends ||= {} raise "Provide a configuration block" unless block_given? if named_append_or_override_exists?(name) raise AlreadyConfiguredError, "Configuration already exists" end @appends[name] = block end def dup default_config.dup end private def named_append_or_override_exists?(name) (defined?(@appends) && @appends.key?(name)) || (defined?(@overrides) && @overrides.key?(name)) end # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) return unless config config.each_with_object({}) do |(key, value), hash| hash[key] = if value.is_a?(Array) value.dup else value end end end # Private: Returns the internal default configuration. This should only # ever be called by internal callers (or tests) that know the semantics # of ensuring that the default config is never mutated and is dup(ed) # before it is used in a request. def default_config unless defined?(@default_config) raise NotYetConfiguredError, "Default policy not yet configured" end @default_config end # Private: convenience method purely DRY things up. The value may not be a # hash (e.g. OPT_OUT, nil) def deep_copy_if_hash(value) if value.is_a?(Hash) deep_copy(value) else value end end end CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = { hsts: StrictTransportSecurity, x_frame_options: XFrameOptions, x_content_type_options: XContentTypeOptions, x_xss_protection: XXssProtection, x_download_options: XDownloadOptions, x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies, referrer_policy: ReferrerPolicy, clear_site_data: ClearSiteData, expect_certificate_transparency: ExpectCertificateTransparency, csp: ContentSecurityPolicy, csp_report_only: ContentSecurityPolicy, cookies: Cookie, }.freeze CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze # The list of attributes that must respond to a `validate_config!` method VALIDATABLE_ATTRIBUTES = CONFIG_ATTRIBUTES # The list of attributes that must respond to a `make_header` method HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze attr_writer(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.reject { |key| [:csp, :csp_report_only].include?(key) }.keys)) attr_reader(*(CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)) @script_hashes = nil @style_hashes = nil HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" if File.exist?(HASH_CONFIG_FILE) config = YAML.safe_load(File.open(HASH_CONFIG_FILE)) @script_hashes = config["scripts"] @style_hashes = config["styles"] end def initialize(&block) @cookies = self.class.send(:deep_copy_if_hash, Cookie::COOKIE_DEFAULTS) @clear_site_data = nil @csp = nil @csp_report_only = nil @hsts = nil @x_content_type_options = nil @x_download_options = nil @x_frame_options = nil @x_permitted_cross_domain_policies = nil @x_xss_protection = nil @expect_certificate_transparency = nil self.referrer_policy = OPT_OUT self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) self.csp_report_only = OPT_OUT instance_eval(&block) if block_given? end # Public: copy everything # # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new copy.cookies = self.class.send(:deep_copy_if_hash, @cookies) copy.csp = @csp.dup if @csp copy.csp_report_only = @csp_report_only.dup if @csp_report_only copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts copy.x_frame_options = @x_frame_options copy.x_xss_protection = @x_xss_protection copy.x_download_options = @x_download_options copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies copy.clear_site_data = @clear_site_data copy.expect_certificate_transparency = @expect_certificate_transparency copy.referrer_policy = @referrer_policy copy end # Public: Apply a named override to the current config # # Returns self def override(name = nil, &block) if override = self.class.overrides(name) instance_eval(&override) else raise ArgumentError.new("no override by the name of #{name} has been configured") end self end def generate_headers headers = {} HEADERABLE_ATTRIBUTES.each do |attr| klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] header_name, value = klass.make_header(instance_variable_get("@#{attr}")) if header_name && value headers[header_name] = value end end headers end def opt_out(header) send("#{header}=", OPT_OUT) end def update_x_frame_options(value) @x_frame_options = value end # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. # # Returns nothing def validate_config! VALIDATABLE_ATTRIBUTES.each do |attr| klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr] klass.validate_config!(instance_variable_get("@#{attr}")) end end def secure_cookies=(secure_cookies) raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead." end def csp=(new_csp) case new_csp when OPT_OUT @csp = new_csp when ContentSecurityPolicyConfig @csp = new_csp when Hash @csp = ContentSecurityPolicyConfig.new(new_csp) else raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash" end end # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot # contain `report_only: false` or an error will be raised. # # NOTE: if csp has not been configured/has the default value when # configuring csp_report_only, the code will assume you mean to only use # report-only mode and you will be opted-out of enforce mode. def csp_report_only=(new_csp) case new_csp when OPT_OUT @csp_report_only = new_csp when ContentSecurityPolicyReportOnlyConfig @csp_report_only = new_csp.dup when ContentSecurityPolicyConfig @csp_report_only = new_csp.make_report_only when Hash @csp_report_only = ContentSecurityPolicyReportOnlyConfig.new(new_csp) else raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash" end end end end secure_headers-6.3.2/lib/secure_headers/hash_helper.rb000066400000000000000000000004771401055541100230450ustar00rootroot00000000000000# frozen_string_literal: true require "base64" module SecureHeaders module HashHelper def hash_source(inline_script, digest = :SHA256) base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp "'#{digest.to_s.downcase}-#{base64_hashed_content}'" end end end secure_headers-6.3.2/lib/secure_headers/headers/000077500000000000000000000000001401055541100216415ustar00rootroot00000000000000secure_headers-6.3.2/lib/secure_headers/headers/clear_site_data.rb000066400000000000000000000031741401055541100252760ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class ClearSiteDataConfigError < StandardError; end class ClearSiteData HEADER_NAME = "Clear-Site-Data".freeze # Valid `types` CACHE = "cache".freeze COOKIES = "cookies".freeze STORAGE = "storage".freeze EXECUTION_CONTEXTS = "executionContexts".freeze ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS] class << self # Public: make an Clear-Site-Data header name, value pair # # Returns nil if not configured, returns header name and value if configured. def make_header(config = nil, user_agent = nil) case config when nil, OPT_OUT, [] # noop when Array [HEADER_NAME, make_header_value(config)] when true [HEADER_NAME, make_header_value(ALL_TYPES)] end end def validate_config!(config) case config when nil, OPT_OUT, true # valid when Array unless config.all? { |t| t.is_a?(String) } raise ClearSiteDataConfigError.new("types must be Strings") end else raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") end end # Public: Transform a Clear-Site-Data config (an Array of Strings) into a # String that can be used as the value for the Clear-Site-Data header. # # types - An Array of String of types of data to clear. # # Returns a String of quoted values that are comma separated. def make_header_value(types) types.map { |t| %("#{t}") }.join(", ") end end end end secure_headers-6.3.2/lib/secure_headers/headers/content_security_policy.rb000066400000000000000000000153541401055541100271560ustar00rootroot00000000000000# frozen_string_literal: true require_relative "policy_management" require_relative "content_security_policy_config" module SecureHeaders class ContentSecurityPolicy include PolicyManagement def initialize(config = nil) @config = if config.is_a?(Hash) if config[:report_only] ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) else ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) end elsif config.nil? ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else config end @preserve_schemes = @config.preserve_schemes @script_nonce = @config.script_nonce @style_nonce = @config.style_nonce end ## # Returns the name to use for the header. Either "Content-Security-Policy" or # "Content-Security-Policy-Report-Only" def name @config.class.const_get(:HEADER_NAME) end ## # Return the value of the CSP header def value @value ||= if @config build_value else DEFAULT_VALUE end end private # Private: converts the config object into a string representing a policy. # Places default-src at the first directive and report-uri as the last. All # others are presented in alphabetical order. # # Returns a content security policy header value. def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :sandbox_list build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) end end.compact.join("; ") end def build_sandbox_list_directive(directive) return unless sandbox_list = @config.directive_value(directive) max_strict_policy = case sandbox_list when Array sandbox_list.empty? when true true else false end # A maximally strict sandbox policy is just the `sandbox` directive, # whith no configuraiton values. if max_strict_policy symbol_to_hyphen_case(directive) elsif sandbox_list && sandbox_list.any? [ symbol_to_hyphen_case(directive), sandbox_list.uniq ].join(" ") end end def build_media_type_list_directive(directive) return unless media_type_list = @config.directive_value(directive) if media_type_list && media_type_list.any? [ symbol_to_hyphen_case(directive), media_type_list.uniq ].join(" ") end end # Private: builds a string that represents one directive in a minified form. # # directive_name - a symbol representing the various ALL_DIRECTIVES # # Returns a string representing a directive. def build_source_list_directive(directive) source_list = @config.directive_value(directive) if source_list != OPT_OUT && source_list && source_list.any? minified_source_list = minify_source_list(directive, source_list).join(" ") if minified_source_list =~ /(\n|;)/ Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.") end escaped_source_list = minified_source_list.gsub(/[\n;]/, " ") [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip end end # If a directive contains *, all other values are omitted. # If a directive contains 'none' but has other values, 'none' is ommitted. # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) def minify_source_list(directive, source_list) source_list = source_list.compact if source_list.include?(STAR) keep_wildcard_sources(source_list) else source_list = populate_nonces(directive, source_list) source_list = reject_all_values_if_none(source_list) unless directive == REPORT_URI || @preserve_schemes source_list = strip_source_schemes(source_list) end dedup_source_list(source_list) end end # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. def keep_wildcard_sources(source_list) source_list.select { |value| WILDCARD_SOURCES.include?(value) } end # Discard any 'none' values if more directives are supplied since none may override values. def reject_all_values_if_none(source_list) if source_list.length > 1 source_list.reject { |value| value == NONE } else source_list end end # Removes duplicates and sources that already match an existing wild card. # # e.g. *.github.com asdf.github.com becomes *.github.com def dedup_source_list(sources) sources = sources.uniq wild_sources = sources.select { |source| source =~ STAR_REGEXP } if wild_sources.any? sources.reject do |source| !wild_sources.include?(source) && wild_sources.any? { |pattern| File.fnmatch(pattern, source) } end else sources end end # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. def populate_nonces(directive, source_list) case directive when SCRIPT_SRC append_nonce(source_list, @script_nonce) when STYLE_SRC append_nonce(source_list, @style_nonce) else source_list end end # Private: adds a nonce or 'unsafe-inline' depending on browser support. # If a nonce is populated, inline content is assumed. # # While CSP is backward compatible in that a policy with a nonce will ignore # unsafe-inline, this is more concise. def append_nonce(source_list, nonce) if nonce source_list.push("'nonce-#{nonce}'") source_list.push(UNSAFE_INLINE) unless @config[:disable_nonce_backwards_compatibility] end source_list end # Private: return the list of directives, # starting with default-src and ending with report-uri. def directives [ DEFAULT_SRC, BODY_DIRECTIVES, REPORT_URI, ].flatten end # Private: Remove scheme from source expressions. def strip_source_schemes(source_list) source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end def symbol_to_hyphen_case(sym) sym.to_s.tr("_", "-") end end end secure_headers-6.3.2/lib/secure_headers/headers/content_security_policy_config.rb000066400000000000000000000074451401055541100305050ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders module DynamicConfig def self.included(base) base.send(:attr_reader, *base.attrs) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| if self.class.attrs.include?(attr) write_attribute(attr, value) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}" end end end end def initialize(hash) @base_uri = nil @block_all_mixed_content = nil @child_src = nil @connect_src = nil @default_src = nil @font_src = nil @form_action = nil @frame_ancestors = nil @frame_src = nil @img_src = nil @manifest_src = nil @media_src = nil @navigate_to = nil @object_src = nil @plugin_types = nil @prefetch_src = nil @preserve_schemes = nil @report_only = nil @report_uri = nil @require_sri_for = nil @sandbox = nil @script_nonce = nil @script_src = nil @script_src_elem = nil @script_src_attr = nil @style_nonce = nil @style_src = nil @style_src_elem = nil @style_src_attr = nil @worker_src = nil @upgrade_insecure_requests = nil @disable_nonce_backwards_compatibility = nil from_hash(hash) end def update_directive(directive, value) self.send("#{directive}=", value) end def directive_value(directive) if self.class.attrs.include?(directive) self.send(directive) end end def merge(new_hash) new_config = self.dup new_config.send(:from_hash, new_hash) new_config end def merge!(new_hash) from_hash(new_hash) end def append(new_hash) from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash)) end def to_h self.class.attrs.each_with_object({}) do |key, hash| value = self.send(key) hash[key] = value unless value.nil? end end def dup self.class.new(self.to_h) end def opt_out? false end def ==(o) self.class == o.class && self.to_h == o.to_h end alias_method :[], :directive_value alias_method :[]=, :update_directive private def from_hash(hash) hash.each_pair do |k, v| next if v.nil? if self.class.attrs.include?(k) write_attribute(k, v) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}" end end end def write_attribute(attr, value) value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list attr_variable = "@#{attr}" self.instance_variable_set(attr_variable, value) end end class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig HEADER_NAME = "Content-Security-Policy".freeze ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES def self.attrs ATTRS end include DynamicConfig # based on what was suggested in https://github.com/rails/rails/pull/24961/files DEFAULT = { default_src: %w('self' https:), font_src: %w('self' https: data:), img_src: %w('self' https: data:), object_src: %w('none'), script_src: %w(https:), style_src: %w('self' https: 'unsafe-inline') } def report_only? false end def make_report_only ContentSecurityPolicyReportOnlyConfig.new(self.to_h) end end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig HEADER_NAME = "Content-Security-Policy-Report-Only".freeze def report_only? true end def make_report_only self end end end secure_headers-6.3.2/lib/secure_headers/headers/cookie.rb000066400000000000000000000063011401055541100234370ustar00rootroot00000000000000# frozen_string_literal: true require "cgi" require "secure_headers/utils/cookies_config" module SecureHeaders class CookiesConfigError < StandardError; end class Cookie class << self def validate_config!(config) CookiesConfig.new(config).validate! end end attr_reader :raw_cookie, :config COOKIE_DEFAULTS = { httponly: true, secure: true, samesite: { lax: true }, }.freeze def initialize(cookie, config) @raw_cookie = cookie unless config == OPT_OUT config ||= {} config = COOKIE_DEFAULTS.merge(config) end @config = config @attributes = { httponly: nil, samesite: nil, secure: nil, } parse(cookie) end def to_s @raw_cookie.dup.tap do |c| c << "; secure" if secure? c << "; HttpOnly" if httponly? c << "; #{samesite_cookie}" if samesite? end end def secure? flag_cookie?(:secure) && !already_flagged?(:secure) end def httponly? flag_cookie?(:httponly) && !already_flagged?(:httponly) end def samesite? flag_samesite? && !already_flagged?(:samesite) end private def parsed_cookie @parsed_cookie ||= CGI::Cookie.parse(raw_cookie) end def already_flagged?(attribute) @attributes[attribute] end def flag_cookie?(attribute) return false if config == OPT_OUT case config[attribute] when TrueClass true when Hash conditionally_flag?(config[attribute]) else false end end def conditionally_flag?(configuration) if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) true elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) true else false end end def samesite_cookie if flag_samesite_lax? "SameSite=Lax" elsif flag_samesite_strict? "SameSite=Strict" elsif flag_samesite_none? "SameSite=None" end end def flag_samesite? return false if config == OPT_OUT || config[:samesite] == OPT_OUT flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none? end def flag_samesite_lax? flag_samesite_enforcement?(:lax) end def flag_samesite_strict? flag_samesite_enforcement?(:strict) end def flag_samesite_none? flag_samesite_enforcement?(:none) end def flag_samesite_enforcement?(mode) return unless config[:samesite] if config[:samesite].is_a?(TrueClass) && mode == :lax return true end case config[:samesite][mode] when Hash conditionally_flag?(config[:samesite][mode]) when TrueClass true else false end end def parse(cookie) return unless cookie cookie.split(/[;,]\s?/).each do |pairs| name, values = pairs.split("=", 2) name = CGI.unescape(name) attribute = name.downcase.to_sym if @attributes.has_key?(attribute) @attributes[attribute] = values || true end end end end end secure_headers-6.3.2/lib/secure_headers/headers/expect_certificate_transparency.rb000066400000000000000000000042461401055541100306170ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class ExpectCertificateTransparencyConfigError < StandardError; end class ExpectCertificateTransparency HEADER_NAME = "Expect-CT".freeze INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze class << self # Public: Generate a Expect-CT header. # # Returns nil if not configured, returns header name and value if # configured. def make_header(config, use_agent = nil) return if config.nil? || config == OPT_OUT header = new(config) [HEADER_NAME, header.value] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash unless [true, false, nil].include?(config[:enforce]) raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR) end if !config[:max_age] raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR) elsif config[:max_age].to_s !~ /\A\d+\z/ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR) end end end def initialize(config) @enforced = config.fetch(:enforce, nil) @max_age = config.fetch(:max_age, nil) @report_uri = config.fetch(:report_uri, nil) end def value [ enforced_directive, max_age_directive, report_uri_directive ].compact.join(", ").strip end def enforced_directive # Unfortunately `if @enforced` isn't enough here in case someone # passes in a random string so let's be specific with it to prevent # accidental enforcement. "enforce" if @enforced == true end def max_age_directive "max-age=#{@max_age}" if @max_age end def report_uri_directive "report-uri=\"#{@report_uri}\"" if @report_uri end end end secure_headers-6.3.2/lib/secure_headers/headers/policy_management.rb000066400000000000000000000354151401055541100256710ustar00rootroot00000000000000# frozen_string_literal: true require "set" module SecureHeaders module PolicyManagement def self.included(base) base.extend(ClassMethods) end DEFAULT_CONFIG = { default_src: %w(https:), img_src: %w(https: data: 'self'), object_src: %w('none'), script_src: %w(https:), style_src: %w('self' 'unsafe-inline' https:), form_action: %w('self') }.freeze DATA_PROTOCOL = "data:".freeze BLOB_PROTOCOL = "blob:".freeze SELF = "'self'".freeze NONE = "'none'".freeze STAR = "*".freeze UNSAFE_INLINE = "'unsafe-inline'".freeze UNSAFE_EVAL = "'unsafe-eval'".freeze STRICT_DYNAMIC = "'strict-dynamic'".freeze # leftover deprecated values that will be in common use upon upgrading. DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze DEFAULT_SRC = :default_src CONNECT_SRC = :connect_src FONT_SRC = :font_src FRAME_SRC = :frame_src IMG_SRC = :img_src MEDIA_SRC = :media_src OBJECT_SRC = :object_src SANDBOX = :sandbox SCRIPT_SRC = :script_src STYLE_SRC = :style_src REPORT_URI = :report_uri DIRECTIVES_1_0 = [ DEFAULT_SRC, CONNECT_SRC, FONT_SRC, FRAME_SRC, IMG_SRC, MEDIA_SRC, OBJECT_SRC, SANDBOX, SCRIPT_SRC, STYLE_SRC, REPORT_URI ].freeze BASE_URI = :base_uri CHILD_SRC = :child_src FORM_ACTION = :form_action FRAME_ANCESTORS = :frame_ancestors PLUGIN_TYPES = :plugin_types DIRECTIVES_2_0 = [ DIRECTIVES_1_0, BASE_URI, CHILD_SRC, FORM_ACTION, FRAME_ANCESTORS, PLUGIN_TYPES ].flatten.freeze # All the directives currently under consideration for CSP level 3. # https://w3c.github.io/webappsec/specs/CSP2/ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src NAVIGATE_TO = :navigate_to PREFETCH_SRC = :prefetch_src REQUIRE_SRI_FOR = :require_sri_for UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests WORKER_SRC = :worker_src SCRIPT_SRC_ELEM = :script_src_elem SCRIPT_SRC_ATTR = :script_src_attr STYLE_SRC_ELEM = :style_src_elem STYLE_SRC_ATTR = :style_src_attr DIRECTIVES_3_0 = [ DIRECTIVES_2_0, BLOCK_ALL_MIXED_CONTENT, MANIFEST_SRC, NAVIGATE_TO, PREFETCH_SRC, REQUIRE_SRI_FOR, WORKER_SRC, UPGRADE_INSECURE_REQUESTS, SCRIPT_SRC_ELEM, SCRIPT_SRC_ATTR, STYLE_SRC_ELEM, STYLE_SRC_ATTR ].flatten.freeze ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] DIRECTIVE_VALUE_TYPES = { BASE_URI => :source_list, BLOCK_ALL_MIXED_CONTENT => :boolean, CHILD_SRC => :source_list, CONNECT_SRC => :source_list, DEFAULT_SRC => :source_list, FONT_SRC => :source_list, FORM_ACTION => :source_list, FRAME_ANCESTORS => :source_list, FRAME_SRC => :source_list, IMG_SRC => :source_list, MANIFEST_SRC => :source_list, MEDIA_SRC => :source_list, NAVIGATE_TO => :source_list, OBJECT_SRC => :source_list, PLUGIN_TYPES => :media_type_list, REQUIRE_SRI_FOR => :require_sri_for_list, REPORT_URI => :source_list, PREFETCH_SRC => :source_list, SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, SCRIPT_SRC_ELEM => :source_list, SCRIPT_SRC_ATTR => :source_list, STYLE_SRC => :source_list, STYLE_SRC_ELEM => :source_list, STYLE_SRC_ATTR => :source_list, WORKER_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean, }.freeze # These are directives that don't have use a source list, and hence do not # inherit the default-src value. NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type| type != :source_list end.keys.freeze # These are directives that take a source list, but that do not inherit # the default-src value. NON_FETCH_SOURCES = [ BASE_URI, FORM_ACTION, FRAME_ANCESTORS, NAVIGATE_TO, REPORT_URI, ] FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} WILDCARD_SOURCES = [ UNSAFE_EVAL, UNSAFE_INLINE, STAR, DATA_PROTOCOL, BLOB_PROTOCOL ].freeze META_CONFIGS = [ :report_only, :preserve_schemes, :disable_nonce_backwards_compatibility ].freeze NONCES = [ :script_nonce, :style_nonce ].freeze REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style)) module ClassMethods # Public: generate a header name, value array that is user-agent-aware. # # Returns a default policy if no configuration is provided, or a # header name and value based on the config. def make_header(config) return if config.nil? || config == OPT_OUT header = new(config) [header.name, header.value] end # Public: Validates each source expression. # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) if config.directive_value(:script_src).nil? raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override") end if !config.report_only? && config.directive_value(:report_only) raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should set :report_only to true") end if config.report_only? && config.directive_value(:report_only) == false raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true") end ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value if META_CONFIGS.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? elsif NONCES.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil? else validate_directive!(key, value) end end end # Public: combine the values from two different configs. # # original - the main config # additions - values to be merged in # # raises an error if the original config is OPT_OUT # # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), # additions will overwrite the original value. # 2. if a value in additions does not exist in the original config, the # default-src value is included to match original behavior. # 3. if a value in additions does exist in the original config, the two # values are joined. def combine_policies(original, additions) if original == {} raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end original = Configuration.send(:deep_copy, original) populate_fetch_source_with_default!(original, additions) merge_policy_additions(original, additions) end def ua_to_variation(user_agent) family = user_agent.browser if family && VARIATIONS.key?(family) family else OTHER end end private # merge the two hashes. combine (instead of overwrite) the array values # when each hash contains a value for a given key. def merge_policy_additions(original, additions) original.merge(additions) do |directive, lhs, rhs| if list_directive?(directive) (lhs.to_a + rhs.to_a).compact.uniq else rhs end end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. end # Returns True if a directive expects a list of values and False otherwise. def list_directive?(directive) source_list?(directive) || sandbox_list?(directive) || media_type_list?(directive) || require_sri_for_list?(directive) end # For each directive in additions that does not exist in the original config, # copy the default-src value to the original config. This modifies the original hash. def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.each_key do |directive| directive = if directive.to_s.end_with?("_nonce") directive.to_s.gsub(/_nonce/, "_src").to_sym else directive end # Don't set a default if directive has an existing value next if original[directive] if FETCH_SOURCES.include?(directive) original[directive] = original[DEFAULT_SRC] end end end def source_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :source_list end def sandbox_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list end def media_type_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :media_type_list end def require_sri_for_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list end # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. def validate_directive!(directive, value) ensure_valid_directive!(directive) case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive] when :source_list validate_source_expression!(directive, value) when :boolean unless boolean?(value) raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value") end when :sandbox_list validate_sandbox_expression!(directive, value) when :media_type_list validate_media_type_expression!(directive, value) when :require_sri_for_list validate_require_sri_source_expression!(directive, value) else raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end end # Private: validates that a sandbox token expression: # 1. is an array of strings or optionally `true` (to enable maximal sandboxing) # 2. For arrays, each element is of the form allow-* def validate_sandbox_expression!(directive, sandbox_token_expression) # We support sandbox: true to indicate a maximally secure sandbox. return if boolean?(sandbox_token_expression) && sandbox_token_expression == true ensure_array_of_strings!(directive, sandbox_token_expression) valid = sandbox_token_expression.compact.all? do |v| v.is_a?(String) && v.start_with?("allow-") end if !valid raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)") end end # Private: validates that a media type expression: # 1. is an array of strings # 2. each element is of the form type/subtype def validate_media_type_expression!(directive, media_type_expression) ensure_array_of_strings!(directive, media_type_expression) valid = media_type_expression.compact.all? do |v| # All media types are of the form: "/" . v =~ /\A.+\/.+\z/ end if !valid raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)") end end # Private: validates that a require sri for expression: # 1. is an array of strings # 2. is a subset of ["string", "style"] def validate_require_sri_source_expression!(directive, require_sri_for_expression) ensure_array_of_strings!(directive, require_sri_for_expression) unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES) raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression})) end end # Private: validates that a source expression: # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_source_expression!(directive, source_expression) if source_expression != OPT_OUT ensure_array_of_strings!(directive, source_expression) end ensure_valid_sources!(directive, source_expression) end def ensure_valid_directive!(directive) unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(directive) raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end end def ensure_array_of_strings!(directive, value) if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) }) raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings") end end def ensure_valid_sources!(directive, source_expression) return if source_expression == OPT_OUT source_expression.each do |expression| if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression) raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.") end end end def boolean?(source_expression) source_expression.is_a?(TrueClass) || source_expression.is_a?(FalseClass) end end end end secure_headers-6.3.2/lib/secure_headers/headers/referrer_policy.rb000066400000000000000000000025141401055541100253630ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class ReferrerPolicyConfigError < StandardError; end class ReferrerPolicy HEADER_NAME = "Referrer-Policy".freeze DEFAULT_VALUE = "origin-when-cross-origin" VALID_POLICIES = %w( no-referrer no-referrer-when-downgrade same-origin strict-origin strict-origin-when-cross-origin origin origin-when-cross-origin unsafe-url ) class << self # Public: generate an Referrer Policy header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT config ||= DEFAULT_VALUE [HEADER_NAME, Array(config).join(", ")] end def validate_config!(config) case config when nil, OPT_OUT # valid when String, Array config = Array(config) unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) } raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}") end else raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}") end end end end end secure_headers-6.3.2/lib/secure_headers/headers/strict_transport_security.rb000066400000000000000000000021271401055541100275430ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class STSConfigError < StandardError; end class StrictTransportSecurity HEADER_NAME = "Strict-Transport-Security".freeze HSTS_MAX_AGE = "631138519" DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}" class << self # Public: generate an hsts header name, value pair. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String) raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER end end end end secure_headers-6.3.2/lib/secure_headers/headers/x_content_type_options.rb000066400000000000000000000016671401055541100270150ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class XContentTypeOptionsConfigError < StandardError; end class XContentTypeOptions HEADER_NAME = "X-Content-Type-Options".freeze DEFAULT_VALUE = "nosniff" class << self # Public: generate an X-Content-Type-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config.casecmp(DEFAULT_VALUE) == 0 raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'") end end end end end secure_headers-6.3.2/lib/secure_headers/headers/x_download_options.rb000066400000000000000000000016111401055541100260760ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class XDOConfigError < StandardError; end class XDownloadOptions HEADER_NAME = "X-Download-Options".freeze DEFAULT_VALUE = "noopen" class << self # Public: generate an X-Download-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config.casecmp(DEFAULT_VALUE) == 0 raise XDOConfigError.new("Value can only be nil or 'noopen'") end end end end end secure_headers-6.3.2/lib/secure_headers/headers/x_frame_options.rb000066400000000000000000000021161401055541100253620ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions HEADER_NAME = "X-Frame-Options".freeze SAMEORIGIN = "sameorigin" DENY = "deny" ALLOW_FROM = "allow-from" ALLOW_ALL = "allowall" DEFAULT_VALUE = SAMEORIGIN VALID_XFO_HEADER = /\A(#{SAMEORIGIN}\z|#{DENY}\z|#{ALLOW_ALL}\z|#{ALLOW_FROM}[:\s])/i class << self # Public: generate an X-Frame-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config =~ VALID_XFO_HEADER raise XFOConfigError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:|ALLOWALL") end end end end end secure_headers-6.3.2/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb000066400000000000000000000020361401055541100311420ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class XPCDPConfigError < StandardError; end class XPermittedCrossDomainPolicies HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze DEFAULT_VALUE = "none" VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) class << self # Public: generate an X-Permitted-Cross-Domain-Policies header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless VALID_POLICIES.include?(config.downcase) raise XPCDPConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") end end end end end secure_headers-6.3.2/lib/secure_headers/headers/x_xss_protection.rb000066400000000000000000000017201401055541100256000ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection HEADER_NAME = "X-XSS-Protection".freeze DEFAULT_VALUE = "1; mode=block" VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/ class << self # Public: generate an X-Xss-Protection header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil, user_agent = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) raise XXssProtectionConfigError.new("Invalid format (see VALID_X_XSS_HEADER)") unless config.to_s =~ VALID_X_XSS_HEADER end end end end secure_headers-6.3.2/lib/secure_headers/middleware.rb000066400000000000000000000030771401055541100226770ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class Middleware def initialize(app) @app = app end # merges the hash of headers into the current header set. def call(env) req = Rack::Request.new(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end private # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 def flag_cookies!(headers, config) if cookies = headers["Set-Cookie"] # Support Rails 2.3 / Rack 1.1 arrays as headers cookies = cookies.split("\n") unless cookies.is_a?(Array) headers["Set-Cookie"] = cookies.map do |cookie| SecureHeaders::Cookie.new(cookie, config).to_s end.join("\n") end end # disable Secure cookies for non-https requests def override_secure(env, config = {}) if scheme(env) != "https" && config != OPT_OUT config[:secure] = OPT_OUT end config end # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119 def scheme(env) if env["HTTPS"] == "on" || env["HTTP_X_SSL_REQUEST"] == "on" "https" elsif env["HTTP_X_FORWARDED_PROTO"] env["HTTP_X_FORWARDED_PROTO"].split(",")[0] else env["rack.url_scheme"] end end end end secure_headers-6.3.2/lib/secure_headers/railtie.rb000066400000000000000000000026421401055541100222100ustar00rootroot00000000000000# frozen_string_literal: true # rails 3.1+ if defined?(Rails::Railtie) module SecureHeaders class Railtie < Rails::Railtie isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 conflicting_headers = ["X-Frame-Options", "X-XSS-Protection", "X-Permitted-Cross-Domain-Policies", "X-Download-Options", "X-Content-Type-Options", "Strict-Transport-Security", "Content-Security-Policy", "Content-Security-Policy-Report-Only", "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware end rake_tasks do load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__)) end initializer "secure_headers.action_controller" do ActiveSupport.on_load(:action_controller) do include SecureHeaders unless Rails.application.config.action_dispatch.default_headers.nil? conflicting_headers.each do |header| Rails.application.config.action_dispatch.default_headers.delete(header) end end end end end end else module ActionController class Base include SecureHeaders end end end secure_headers-6.3.2/lib/secure_headers/utils/000077500000000000000000000000001401055541100213665ustar00rootroot00000000000000secure_headers-6.3.2/lib/secure_headers/utils/cookies_config.rb000066400000000000000000000100711401055541100246730ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders class CookiesConfig attr_reader :config def initialize(config) @config = config end def validate! return if config.nil? || config == SecureHeaders::OPT_OUT validate_config! validate_secure_config! unless config[:secure].nil? validate_httponly_config! unless config[:httponly].nil? validate_samesite_config! unless config[:samesite].nil? end private def validate_config! raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config) end def validate_secure_config! validate_hash_or_true_or_opt_out!(:secure) validate_exclusive_use_of_hash_constraints!(config[:secure], :secure) end def validate_httponly_config! validate_hash_or_true_or_opt_out!(:httponly) validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly) end def validate_samesite_config! return if config[:samesite] == OPT_OUT raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite]) validate_samesite_boolean_config! validate_samesite_hash_config! end # when configuring with booleans, only one enforcement is permitted def validate_samesite_boolean_config! if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none)) raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.") elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none)) raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.") elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict)) raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.") end end def validate_samesite_hash_config! # validate Hash-based samesite configuration if is_hash?(config[:samesite][:lax]) validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax") if is_hash?(config[:samesite][:strict]) validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict") validate_exclusive_use_of_samesite_enforcement!(:only) validate_exclusive_use_of_samesite_enforcement!(:except) end end end def validate_hash_or_true_or_opt_out!(attribute) if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute])) raise CookiesConfigError.new("#{attribute} cookie config must be a hash, true, or SecureHeaders::OPT_OUT") end end # validate exclusive use of only or except but not both at the same time def validate_exclusive_use_of_hash_constraints!(conf, attribute) return unless is_hash?(conf) if conf.key?(:only) && conf.key?(:except) raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") end end # validate exclusivity of only and except members within strict and lax def validate_exclusive_use_of_samesite_enforcement!(attribute) if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any? raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") end end def is_hash?(obj) obj && obj.is_a?(Hash) end def is_true_or_opt_out?(obj) obj && (obj.is_a?(TrueClass) || obj == OPT_OUT) end end end secure_headers-6.3.2/lib/secure_headers/version.rb000066400000000000000000000001141401055541100222340ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders VERSION = "6.3.2" end secure_headers-6.3.2/lib/secure_headers/view_helper.rb000066400000000000000000000135561401055541100230760ustar00rootroot00000000000000# frozen_string_literal: true module SecureHeaders module ViewHelpers include SecureHeaders::HashHelper SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes" class UnexpectedHashedScriptException < StandardError; end # Public: create a style tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style-src directive. # # Returns an html-safe style tag with the nonce attribute. def nonced_style_tag(content_or_options = {}, &block) nonced_tag(:style, content_or_options, block) end # Public: create a stylesheet link tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style-src directive. # # Returns an html-safe link tag with the nonce attribute. def nonced_stylesheet_link_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) stylesheet_link_tag(*args, **opts, &block) end # Public: create a script tag using the content security policy nonce. # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_tag(content_or_options = {}, &block) nonced_tag(:script, content_or_options, block) end # Public: create a script src tag using the content security policy nonce. # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_include_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) javascript_include_tag(*args, **opts, &block) end # Public: create a script Webpacker pack tag using the content security policy nonce. # Instructs secure_headers to append a nonce to script-src directive. # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_pack_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script)) javascript_pack_tag(*args, **opts, &block) end # Public: create a stylesheet Webpacker link tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style-src directive. # # Returns an html-safe link tag with the nonce attribute. def nonced_stylesheet_pack_tag(*args, &block) opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style)) stylesheet_pack_tag(*args, **opts, &block) end # Public: use the content security policy nonce for this request directly. # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns a non-html-safe nonce value. def _content_security_policy_nonce(type) case type when :script SecureHeaders.content_security_policy_script_nonce(@_request) when :style SecureHeaders.content_security_policy_style_nonce(@_request) end end alias_method :content_security_policy_nonce, :_content_security_policy_nonce def content_security_policy_script_nonce _content_security_policy_nonce(:script) end def content_security_policy_style_nonce _content_security_policy_nonce(:style) end ## # Checks to see if the hashed code is expected and adds the hash source # value to the current CSP. # # By default, in development/test/etc. an exception will be raised. def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) hashed_tag( :script, :script_src, Configuration.instance_variable_get(:@script_hashes), raise_error_on_unrecognized_hash, block ) end def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block) hashed_tag( :style, :style_src, Configuration.instance_variable_get(:@style_hashes), raise_error_on_unrecognized_hash, block ) end private def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) if raise_error_on_unrecognized_hash.nil? raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production" end content = capture(&block) file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb") if raise_error_on_unrecognized_hash hash_value = hash_source(content) message = unexpected_hash_error_message(file_path, content, hash_value) if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value) raise UnexpectedHashedScriptException.new(message) end end SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path]) content_tag type, content end def unexpected_hash_error_message(file_path, content, hash_value) <<-EOF \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** #{content} *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:*** #{file_path}: - \"#{hash_value}\"\n\n NOTE: dynamic javascript is not supported using script hash integration on purpose. It defeats the point of using it in the first place. EOF end def nonced_tag(type, content_or_options, block) options = {} content = if block options = content_or_options capture(&block) else content_or_options.html_safe # :'( end content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type)) end def extract_options(args) if args.last.is_a? Hash args.pop else {} end end end end ActiveSupport.on_load :action_view do include SecureHeaders::ViewHelpers end if defined?(ActiveSupport) secure_headers-6.3.2/lib/tasks/000077500000000000000000000000001401055541100163725ustar00rootroot00000000000000secure_headers-6.3.2/lib/tasks/tasks.rake000066400000000000000000000051231401055541100203640ustar00rootroot00000000000000# frozen_string_literal: true INLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX namespace :secure_headers do include SecureHeaders::HashHelper def is_erb?(filename) filename =~ /\.erb\Z/ end def is_mustache?(filename) filename =~ /\.mustache\Z/ end def dynamic_content?(filename, inline_script) (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/) end def find_inline_content(filename, regex, hashes) file = File.read(filename) file.scan(regex) do # TODO don't use gsub inline_script = Regexp.last_match.captures.last if dynamic_content?(filename, inline_script) puts "Looks like there's some dynamic content inside of a tag :-/" puts "That pretty much means the hash value will never match." puts "Code: " + inline_script puts "=" * 20 end hashes << hash_source(inline_script) end end def generate_inline_script_hashes(filename) hashes = [] [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex| find_inline_content(filename, regex, hashes) end hashes end def generate_inline_style_hashes(filename) hashes = [] [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex| find_inline_content(filename, regex, hashes) end hashes end desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" task :generate_hashes do |t, args| script_hashes = { "scripts" => {}, "styles" => {} } Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename| hashes = generate_inline_script_hashes(filename) if hashes.any? script_hashes["scripts"][filename] = hashes end hashes = generate_inline_style_hashes(filename) if hashes.any? script_hashes["styles"][filename] = hashes end end File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file| file.write(script_hashes.to_yaml) end puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" end end secure_headers-6.3.2/secure_headers.gemspec000066400000000000000000000020021401055541100210170ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "secure_headers/version" Gem::Specification.new do |gem| gem.name = "secure_headers" gem.version = SecureHeaders::VERSION gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = "Manages application of security headers with many safe defaults." gem.summary = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' gem.homepage = "https://github.com/twitter/secureheaders" gem.license = "Apache Public License 2.0" gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" end secure_headers-6.3.2/spec/000077500000000000000000000000001401055541100154315ustar00rootroot00000000000000secure_headers-6.3.2/spec/lib/000077500000000000000000000000001401055541100161775ustar00rootroot00000000000000secure_headers-6.3.2/spec/lib/secure_headers/000077500000000000000000000000001401055541100211605ustar00rootroot00000000000000secure_headers-6.3.2/spec/lib/secure_headers/configuration_spec.rb000066400000000000000000000070651401055541100253760ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe Configuration do before(:each) do reset_config end it "has a default config" do expect(Configuration.default).to_not be_nil end it "has an 'noop' override" do Configuration.default expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil end it "dup results in a copy of the default config" do Configuration.default original_configuration = Configuration.send(:default_config) configuration = Configuration.dup expect(original_configuration).not_to be(configuration) Configuration::CONFIG_ATTRIBUTES.each do |attr| expect(original_configuration.send(attr)).to eq(configuration.send(attr)) end end it "stores an override" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" end expect(Configuration.overrides(:test_override)).to_not be_nil end describe "#override" do it "raises on configuring an existing override" do set_override = Proc.new { Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" end } set_override.call expect { set_override.call } .to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") end it "raises when a named append with the given name exists" do Configuration.named_append(:test_override) do |config| config.x_frame_options = "DENY" end expect do Configuration.override(:test_override) do |config| config.x_frame_options = "SAMEORIGIN" end end.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") end end describe "#named_append" do it "raises on configuring an existing append" do set_override = Proc.new { Configuration.named_append(:test_override) do |config| config.x_frame_options = "DENY" end } set_override.call expect { set_override.call } .to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") end it "raises when an override with the given name exists" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" end expect do Configuration.named_append(:test_override) do |config| config.x_frame_options = "SAMEORIGIN" end end.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already exists") end end it "deprecates the secure_cookies configuration" do expect { Configuration.default do |config| config.secure_cookies = true end }.to raise_error(ArgumentError) end it "gives cookies a default config" do expect(Configuration.default.cookies).to eq({httponly: true, secure: true, samesite: {lax: true}}) end it "allows OPT_OUT" do Configuration.default do |config| config.cookies = OPT_OUT end config = Configuration.dup expect(config.cookies).to eq(OPT_OUT) end it "allows me to be explicit too" do Configuration.default do |config| config.cookies = {httponly: true, secure: true, samesite: {lax: false}} end config = Configuration.dup expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}}) end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/000077500000000000000000000000001401055541100225735ustar00rootroot00000000000000secure_headers-6.3.2/spec/lib/secure_headers/headers/clear_site_data_spec.rb000066400000000000000000000045241401055541100272420ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe ClearSiteData do describe "make_header" do it "returns nil with nil config" do expect(described_class.make_header).to be_nil end it "returns nil with empty config" do expect(described_class.make_header([])).to be_nil end it "returns nil with opt-out config" do expect(described_class.make_header(OPT_OUT)).to be_nil end it "returns all types with `true` config" do name, value = described_class.make_header(true) expect(name).to eq(ClearSiteData::HEADER_NAME) expect(value).to eq( %("cache", "cookies", "storage", "executionContexts") ) end it "returns specified types" do name, value = described_class.make_header(["foo", "bar"]) expect(name).to eq(ClearSiteData::HEADER_NAME) expect(value).to eq(%("foo", "bar")) end end describe "validate_config!" do it "succeeds for `true` config" do expect do described_class.validate_config!(true) end.not_to raise_error end it "succeeds for `nil` config" do expect do described_class.validate_config!(nil) end.not_to raise_error end it "succeeds for opt-out config" do expect do described_class.validate_config!(OPT_OUT) end.not_to raise_error end it "succeeds for empty config" do expect do described_class.validate_config!([]) end.not_to raise_error end it "succeeds for Array of Strings config" do expect do described_class.validate_config!(["foo"]) end.not_to raise_error end it "fails for Array of non-String config" do expect do described_class.validate_config!([1]) end.to raise_error(ClearSiteDataConfigError) end it "fails for other types of config" do expect do described_class.validate_config!(:cookies) end.to raise_error(ClearSiteDataConfigError) end end describe "make_header_value" do it "returns a string of quoted values that are comma separated" do value = described_class.make_header_value(["foo", "bar"]) expect(value).to eq(%("foo", "bar")) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/content_security_policy_spec.rb000066400000000000000000000215631401055541100311210ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe ContentSecurityPolicy do let (:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), style_src: %w('unsafe-inline' https: about:), report_uri: %w(/csp_report) } end describe "#name" do context "when in report-only mode" do specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) } end context "when in enforce mode" do specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) } end end describe "#value" do it "uses a safe but non-breaking default value" do expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:") end it "deprecates and escapes semicolons in directive source lists" do expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a ; in "google.com;script-src *;.;" which will raise an error in future versions. It has been replaced with a blank space.)) expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .") end it "deprecates and escapes semicolons in directive source lists" do expect(Kernel).to receive(:warn).with(%(frame_ancestors contains a \n in "\\nfoo.com\\nhacked" which will raise an error in future versions. It has been replaced with a blank space.)) expect(ContentSecurityPolicy.new(frame_ancestors: ["\nfoo.com\nhacked"]).value).to eq("frame-ancestors foo.com hacked") end it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") end it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:)) expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:") end it "minifies source expressions based on overlapping wildcards" do config = { default_src: %w(a.example.org b.example.org *.example.org https://*.example.org) } csp = ContentSecurityPolicy.new(config) expect(csp.value).to eq("default-src *.example.org") end it "removes http/s schemes from hosts" do csp = ContentSecurityPolicy.new(default_src: %w(https://example.org)) expect(csp.value).to eq("default-src example.org") end it "does not build directives with a value of OPT_OUT (and bypasses directive requirements)" do csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), script_src: OPT_OUT) expect(csp.value).to eq("default-src example.org") end it "does not remove schemes from report-uri values" do csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org)) expect(csp.value).to eq("default-src https:; report-uri https://example.org") end it "does not remove schemes when :preserve_schemes is true" do csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), preserve_schemes: true) expect(csp.value).to eq("default-src https://example.org") end it "removes nil from source lists" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil]) expect(csp.value).to eq("default-src example.org") end it "does not add a directive if the value is an empty array (or all nil)" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil]) expect(csp.value).to eq("default-src example.org") end it "does not add a directive if the value is nil" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil) expect(csp.value).to eq("default-src example.org") end it "does add a boolean directive if the value is true" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true) expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests") end it "does not add a boolean directive if the value is false" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false) expect(csp.value).to eq("default-src example.org; block-all-mixed-content") end it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") end it "creates maximally strict sandbox policy when passed no sandbox token values" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: []) expect(csp.value).to eq("default-src example.org; sandbox") end it "creates maximally strict sandbox policy when passed true" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true) expect(csp.value).to eq("default-src example.org; sandbox") end it "creates sandbox policy when passed valid sandbox token values" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts)) expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts") end it "does not emit a warning when using frame-src" do expect(Kernel).to_not receive(:warn) ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end it "allows script as a require-sri-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script)) expect(csp.value).to eq("default-src 'self'; require-sri-for script") end it "allows style as a require-sri-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(style)) expect(csp.value).to eq("default-src 'self'; require-sri-for style") end it "allows script and style as a require-sri-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script style)) expect(csp.value).to eq("default-src 'self'; require-sri-for script style") end it "includes prefetch-src" do csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com)) expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com") end it "includes navigate-to" do csp = ContentSecurityPolicy.new(default_src: %w('self'), navigate_to: %w(foo.com)) expect(csp.value).to eq("default-src 'self'; navigate-to foo.com") end it "supports strict-dynamic" do csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'") end it "supports strict-dynamic and opting out of the appended 'unsafe-inline'" do csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456, disable_nonce_backwards_compatibility: true }) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") end it "supports script-src-elem directive" do csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_elem: %w('self')}) expect(csp.value).to eq("script-src 'self'; script-src-elem 'self'") end it "supports script-src-attr directive" do csp = ContentSecurityPolicy.new({script_src: %w('self'), script_src_attr: %w('self')}) expect(csp.value).to eq("script-src 'self'; script-src-attr 'self'") end it "supports style-src-elem directive" do csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_elem: %w('self')}) expect(csp.value).to eq("style-src 'self'; style-src-elem 'self'") end it "supports style-src-attr directive" do csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_attr: %w('self')}) expect(csp.value).to eq("style-src 'self'; style-src-attr 'self'") end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/cookie_spec.rb000066400000000000000000000163731401055541100254150ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe Cookie do let(:raw_cookie) { "_session=thisisatest" } it "does not tamper with cookies when using OPT_OUT is used" do cookie = Cookie.new(raw_cookie, OPT_OUT) expect(cookie.to_s).to eq(raw_cookie) end it "applies httponly, secure, and samesite by default" do cookie = Cookie.new(raw_cookie, nil) expect(cookie.to_s).to eq("_session=thisisatest; secure; HttpOnly; SameSite=Lax") end it "preserves existing attributes" do cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do cookie = Cookie.new("_session=thisisatest; secure", secure: true, httponly: OPT_OUT) expect(cookie.to_s.scan(/secure/i).count).to eq(1) end context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do cookie = Cookie.new(raw_cookie, secure: true, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }, httponly: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end end context "HttpOnly cookies" do context "when configured with a boolean" do it "flags cookies as HttpOnly" do cookie = Cookie.new(raw_cookie, httponly: true, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }, secure: OPT_OUT, samesite: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end end context "SameSite cookies" do %w(None Lax Strict).each do |flag| it "flags SameSite=#{flag}" do cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { only: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}") end it "flags SameSite=#{flag} when configured with a boolean" do cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true}, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}") end it "does not flag cookies as SameSite=#{flag} when excluded" do cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { except: ["_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest") end end it "flags SameSite=Strict when configured with a boolean" do cookie = Cookie.new(raw_cookie, {samesite: { strict: true}, secure: OPT_OUT, httponly: OPT_OUT}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do raw_cookie = "_session=thisisatest; SameSite=Strict" cookie = Cookie.new(raw_cookie, samesite: { lax: true }, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq(raw_cookie) end it "samesite: true sets all cookies to samesite=lax" do raw_cookie = "_session=thisisatest" cookie = Cookie.new(raw_cookie, samesite: true, secure: OPT_OUT, httponly: OPT_OUT) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end end end context "with an invalid configuration" do it "raises an exception when not configured with a Hash" do expect do Cookie.validate_config!("configuration") end.to raise_error(CookiesConfigError) end it "raises an exception when configured without a boolean(true or OPT_OUT)/Hash" do expect do Cookie.validate_config!(secure: "true") end.to raise_error(CookiesConfigError) end it "raises an exception when configured with false" do expect do Cookie.validate_config!(secure: false) end.to raise_error(CookiesConfigError) end it "raises an exception when both only and except filters are provided" do expect do Cookie.validate_config!(secure: { only: [], except: [] }) end.to raise_error(CookiesConfigError) end it "raises an exception when SameSite is not configured with a Hash" do expect do Cookie.validate_config!(samesite: true) end.to raise_error(CookiesConfigError) end cookie_options = %i(none lax strict) cookie_options.each do |flag| (cookie_options - [flag]).each do |other_flag| it "raises an exception when SameSite #{flag} and #{other_flag} enforcement modes are configured with booleans" do expect do Cookie.validate_config!(samesite: { flag => true, other_flag => true}) end.to raise_error(CookiesConfigError) end end end it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do expect do Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both only and except filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } }) end.to raise_error(CookiesConfigError) end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb000066400000000000000000000036641401055541100325660ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe ExpectCertificateTransparency do specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce, max-age=1234") } specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") } specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") } specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234, report-uri=\"https://report-uri.io/expect-ct\"") } specify do config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" } header_value = "enforce, max-age=1234, report-uri=\"https://report-uri.io/expect-ct\"" expect(ExpectCertificateTransparency.new(config).value).to eq(header_value) end context "with an invalid configuration" do it "raises an exception when configuration isn't a hash" do expect do ExpectCertificateTransparency.validate_config!(%w(a)) end.to raise_error(ExpectCertificateTransparencyConfigError) end it "raises an exception when max-age is not provided" do expect do ExpectCertificateTransparency.validate_config!(foo: "bar") end.to raise_error(ExpectCertificateTransparencyConfigError) end it "raises an exception with an invalid max-age" do expect do ExpectCertificateTransparency.validate_config!(max_age: "abc123") end.to raise_error(ExpectCertificateTransparencyConfigError) end it "raises an exception with an invalid enforce value" do expect do ExpectCertificateTransparency.validate_config!(enforce: "brokenstring") end.to raise_error(ExpectCertificateTransparencyConfigError) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/policy_management_spec.rb000066400000000000000000000253761401055541100276420ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe PolicyManagement do before(:each) do reset_config Configuration.default end let (:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), style_src: %w('unsafe-inline' https: about:), report_uri: %w(/csp_report) } end describe "#validate_config!" do it "accepts all keys" do # (pulled from README) config = { # "meta" values. these will shape the header, but the values are not included in the header. report_only: false, preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), base_uri: %w('self'), block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) connect_src: %w(wss:), child_src: %w('self' *.twimg.com itunes.apple.com), font_src: %w('self' data:), form_action: %w('self' github.com), frame_ancestors: %w('none'), frame_src: %w('self' *.twimg.com itunes.apple.com), img_src: %w(mycdn.com data:), manifest_src: %w(manifest.com), media_src: %w(utoob.com), navigate_to: %w(netscape.com), object_src: %w('self'), plugin_types: %w(application/x-shockwave-flash), prefetch_src: %w(fetch.com), require_sri_for: %w(script style), script_src: %w('self'), style_src: %w('unsafe-inline'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ worker_src: %w(worker.com), script_src_elem: %w(example.com), script_src_attr: %w(example.com), style_src_elem: %w(example.com), style_src_attr: %w(example.com), report_uri: %w(https://example.com/uri-directive), } ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config)) end it "requires a :default_src value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires a :script_src value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "accepts OPT_OUT as a script-src value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: OPT_OUT)) end.to_not raise_error end it "requires :report_only to be a truthy value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :preserve_schemes to be a truthy value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :block_all_mixed_content to be a boolean value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :upgrade_insecure_requests to be a boolean value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires all source lists to be an array of strings" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end it "allows nil values" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil])) end.to_not raise_error end it "rejects unknown directives / config" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "rejects anything not of the form allow-* as a sandbox value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"]))) end.to raise_error(ContentSecurityPolicyConfigError) end it "accepts anything of the form allow-* as a sandbox value " do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"]))) end.to_not raise_error end it "accepts true as a sandbox policy" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true))) end.to_not raise_error end it "rejects anything not of the form type/subtype as a plugin-type value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"]))) end.to raise_error(ContentSecurityPolicyConfigError) end it "accepts anything of the form type/subtype as a plugin-type value " do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"]))) end.to_not raise_error end it "doesn't allow report_only to be set in a non-report-only config" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true))) end.to raise_error(ContentSecurityPolicyConfigError) end it "allows report_only to be set in a report-only config" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true))) end.to_not raise_error end end describe "#combine_policies" do before(:each) do reset_config end it "combines the default-src value with the override if the directive was unconfigured" do Configuration.default do |config| config.csp = { default_src: %w(https:), script_src: %w('self'), } end default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com") end it "combines directives where the original value is nil and the hash is frozen" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self'), report_only: false }.freeze end report_uri = "https://report-uri.io/asdf" default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to include("report-uri #{report_uri}") end it "does not combine the default-src value for directives that don't fall back to default sources" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self'), report_only: false }.freeze end non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) end end it "overrides the report_only flag" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self'), report_only: false } end default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end it "overrides the :block_all_mixed_content flag" do Configuration.default do |config| config.csp = { default_src: %w(https:), script_src: %w('self'), block_all_mixed_content: false } end default_policy = Configuration.dup combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'") end it "raises an error if appending to a OPT_OUT policy" do Configuration.default do |config| config.csp = OPT_OUT end default_policy = Configuration.dup expect do ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/referrer_policy_spec.rb000066400000000000000000000052351401055541100273320ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe ReferrerPolicy do specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) } specify { expect(ReferrerPolicy.make_header("no-referrer")).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } specify { expect(ReferrerPolicy.make_header(%w(origin-when-cross-origin strict-origin-when-cross-origin))).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin, strict-origin-when-cross-origin"]) } context "valid configuration values" do it "accepts 'no-referrer'" do expect do ReferrerPolicy.validate_config!("no-referrer") end.not_to raise_error end it "accepts 'no-referrer-when-downgrade'" do expect do ReferrerPolicy.validate_config!("no-referrer-when-downgrade") end.not_to raise_error end it "accepts 'same-origin'" do expect do ReferrerPolicy.validate_config!("same-origin") end.not_to raise_error end it "accepts 'strict-origin'" do expect do ReferrerPolicy.validate_config!("strict-origin") end.not_to raise_error end it "accepts 'strict-origin-when-cross-origin'" do expect do ReferrerPolicy.validate_config!("strict-origin-when-cross-origin") end.not_to raise_error end it "accepts 'origin'" do expect do ReferrerPolicy.validate_config!("origin") end.not_to raise_error end it "accepts 'origin-when-cross-origin'" do expect do ReferrerPolicy.validate_config!("origin-when-cross-origin") end.not_to raise_error end it "accepts 'unsafe-url'" do expect do ReferrerPolicy.validate_config!("unsafe-url") end.not_to raise_error end it "accepts nil" do expect do ReferrerPolicy.validate_config!(nil) end.not_to raise_error end it "accepts array of policy values" do expect do ReferrerPolicy.validate_config!( %w( origin-when-cross-origin strict-origin-when-cross-origin ) ) end.not_to raise_error end end context "invalid configuration values" do it "doesn't accept invalid values" do expect do ReferrerPolicy.validate_config!("open") end.to raise_error(ReferrerPolicyConfigError) end it "doesn't accept invalid types" do expect do ReferrerPolicy.validate_config!({}) end.to raise_error(TypeError) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/strict_transport_security_spec.rb000066400000000000000000000024121401055541100315040ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe StrictTransportSecurity do describe "#value" do specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) } specify { expect(StrictTransportSecurity.make_header("max-age=1234; includeSubdomains; preload")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234; includeSubdomains; preload"]) } context "with an invalid configuration" do context "with a string argument" do it "raises an exception with an invalid max-age" do expect do StrictTransportSecurity.validate_config!("max-age=abc123") end.to raise_error(STSConfigError) end it "raises an exception if max-age is not supplied" do expect do StrictTransportSecurity.validate_config!("includeSubdomains") end.to raise_error(STSConfigError) end it "raises an exception with an invalid format" do expect do StrictTransportSecurity.validate_config!("max-age=123includeSubdomains") end.to raise_error(STSConfigError) end end end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/x_content_type_options_spec.rb000066400000000000000000000017271401055541100307560ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe XContentTypeOptions do describe "#value" do specify { expect(XContentTypeOptions.make_header).to eq([XContentTypeOptions::HEADER_NAME, XContentTypeOptions::DEFAULT_VALUE]) } specify { expect(XContentTypeOptions.make_header("nosniff")).to eq([XContentTypeOptions::HEADER_NAME, "nosniff"]) } context "invalid configuration values" do it "accepts nosniff" do expect do XContentTypeOptions.validate_config!("nosniff") end.not_to raise_error end it "accepts nil" do expect do XContentTypeOptions.validate_config!(nil) end.not_to raise_error end it "doesn't accept anything besides no-sniff" do expect do XContentTypeOptions.validate_config!("donkey") end.to raise_error(XContentTypeOptionsConfigError) end end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/x_download_options_spec.rb000066400000000000000000000015351401055541100300470ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe XDownloadOptions do specify { expect(XDownloadOptions.make_header).to eq([XDownloadOptions::HEADER_NAME, XDownloadOptions::DEFAULT_VALUE]) } specify { expect(XDownloadOptions.make_header("noopen")).to eq([XDownloadOptions::HEADER_NAME, "noopen"]) } context "invalid configuration values" do it "accepts noopen" do expect do XDownloadOptions.validate_config!("noopen") end.not_to raise_error end it "accepts nil" do expect do XDownloadOptions.validate_config!(nil) end.not_to raise_error end it "doesn't accept anything besides noopen" do expect do XDownloadOptions.validate_config!("open") end.to raise_error(XDOConfigError) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/x_frame_options_spec.rb000066400000000000000000000020611401055541100273250ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe XFrameOptions do describe "#value" do specify { expect(XFrameOptions.make_header).to eq([XFrameOptions::HEADER_NAME, XFrameOptions::DEFAULT_VALUE]) } specify { expect(XFrameOptions.make_header("DENY")).to eq([XFrameOptions::HEADER_NAME, "DENY"]) } context "with invalid configuration" do it "allows SAMEORIGIN" do expect do XFrameOptions.validate_config!("SAMEORIGIN") end.not_to raise_error end it "allows DENY" do expect do XFrameOptions.validate_config!("DENY") end.not_to raise_error end it "allows ALLOW-FROM*" do expect do XFrameOptions.validate_config!("ALLOW-FROM: example.com") end.not_to raise_error end it "does not allow garbage" do expect do XFrameOptions.validate_config!("I like turtles") end.to raise_error(XFOConfigError) end end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb000066400000000000000000000027461401055541100331160ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe XPermittedCrossDomainPolicies do specify { expect(XPermittedCrossDomainPolicies.make_header).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "none"]) } specify { expect(XPermittedCrossDomainPolicies.make_header("master-only")).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "master-only"]) } context "valid configuration values" do it "accepts 'all'" do expect do XPermittedCrossDomainPolicies.validate_config!("all") end.not_to raise_error end it "accepts 'by-ftp-filename'" do expect do XPermittedCrossDomainPolicies.validate_config!("by-ftp-filename") end.not_to raise_error end it "accepts 'by-content-type'" do expect do XPermittedCrossDomainPolicies.validate_config!("by-content-type") end.not_to raise_error end it "accepts 'master-only'" do expect do XPermittedCrossDomainPolicies.validate_config!("master-only") end.not_to raise_error end it "accepts nil" do expect do XPermittedCrossDomainPolicies.validate_config!(nil) end.not_to raise_error end end context "invlaid configuration values" do it "doesn't accept invalid values" do expect do XPermittedCrossDomainPolicies.validate_config!("open") end.to raise_error(XPCDPConfigError) end end end end secure_headers-6.3.2/spec/lib/secure_headers/headers/x_xss_protection_spec.rb000066400000000000000000000032551401055541100275510ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe XXssProtection do specify { expect(XXssProtection.make_header).to eq([XXssProtection::HEADER_NAME, XXssProtection::DEFAULT_VALUE]) } specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, "1; mode=block; report=https://www.secure.com/reports"]) } context "with invalid configuration" do it "should raise an error when providing a string that is not valid" do expect do XXssProtection.validate_config!("asdf") end.to raise_error(XXssProtectionConfigError) expect do XXssProtection.validate_config!("asdf; mode=donkey") end.to raise_error(XXssProtectionConfigError) end context "when using a hash value" do it "should allow string values ('1' or '0' are the only valid strings)" do expect do XXssProtection.validate_config!("1") end.not_to raise_error end it "should raise an error if no value key is supplied" do expect do XXssProtection.validate_config!("mode=block") end.to raise_error(XXssProtectionConfigError) end it "should raise an error if an invalid key is supplied" do expect do XXssProtection.validate_config!("123") end.to raise_error(XXssProtectionConfigError) end it "should raise an error if mode != block" do expect do XXssProtection.validate_config!("1; mode=donkey") end.to raise_error(XXssProtectionConfigError) end end end end end secure_headers-6.3.2/spec/lib/secure_headers/middleware_spec.rb000066400000000000000000000110231401055541100246310ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe Middleware do let(:app) { lambda { |env| [200, env, "app"] } } let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } } let(:middleware) { Middleware.new(app) } let(:cookie_middleware) { Middleware.new(cookie_app) } before(:each) do reset_config Configuration.default end it "sets the headers" do _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {})) expect_default_values(env) end it "respects overrides" do request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") SecureHeaders.override_x_frame_options(request, "DENY") _, env = middleware.call request.env expect(env[XFrameOptions::HEADER_NAME]).to eq("DENY") end it "uses named overrides" do Configuration.override("my_custom_config") do |config| config.csp[:script_src] = %w(example.org) end request = Rack::Request.new({}) SecureHeaders.use_secure_headers_override(request, "my_custom_config") _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end context "cookies" do before(:each) do reset_config end context "cookies should be flagged" do it "flags cookies as secure" do Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") end end it "allows opting out of cookie protection with OPT_OUT alone" do Configuration.default { |config| config.cookies = OPT_OUT } # do NOT make this request https. non-https requests modify a config, # causing an exception when operating on OPT_OUT. This ensures we don't # try to modify the config. request = Rack::Request.new({}) _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") end context "cookies should not be flagged" do it "does not flags cookies as secure" do Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") end end end context "cookies" do before(:each) do reset_config end it "flags cookies from configuration" do Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly; SameSite=Lax") end it "flags cookies with a combination of SameSite configurations" do cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to match("_session=foobar; SameSite=Strict") expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax") end it "disables secure cookies for non-https requests" do Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") end it "sets the secure cookie flag correctly on interleaved http/https requests" do Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar") request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env["Set-Cookie"]).to eq("foo=bar; secure") end end end end secure_headers-6.3.2/spec/lib/secure_headers/view_helpers_spec.rb000066400000000000000000000136001401055541100252130ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "erb" class Message < ERB include SecureHeaders::ViewHelpers def self.template <<-TEMPLATE <% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %> console.log(1) <% end %> <% hashed_style_tag do %> body { background-color: black; } <% end %> <% nonced_javascript_tag do %> body { console.log(1) } <% end %> <% nonced_style_tag do %> body { background-color: black; } <% end %> <%= nonced_javascript_include_tag "include.js", defer: true %> <%= nonced_javascript_pack_tag "pack.js", "otherpack.js", defer: true %> <%= nonced_stylesheet_link_tag "link.css", media: :all %> <%= nonced_stylesheet_pack_tag "pack.css", "otherpack.css", media: :all %> TEMPLATE end def initialize(request, options = {}) @virtual_path = "/asdfs/index" @_request = request @template = self.class.template super(@template) end def capture(*args) yield(*args) end def content_tag(type, content = nil, options = nil, &block) content = if block_given? capture(block) end if options.is_a?(Hash) options = options.map { |k, v| " #{k}=#{v}" } end "<#{type}#{options}>#{content}" end def javascript_include_tag(*sources, **options) sources.map do |source| content_tag(:script, nil, options.merge(src: source)) end end alias_method :javascript_pack_tag, :javascript_include_tag def stylesheet_link_tag(*sources, **options) sources.map do |source| content_tag(:link, nil, options.merge(href: source, rel: "stylesheet", media: "screen")) end end alias_method :stylesheet_pack_tag, :stylesheet_link_tag def result super(binding) end def request @_request end end class MessageWithConflictingMethod < Message def content_security_policy_nonce "rails-nonce" end end module SecureHeaders describe ViewHelpers do let(:app) { lambda { |env| [200, env, "app"] } } let(:middleware) { Middleware.new(app) } let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) } let(:filename) { "app/views/asdfs/index.html.erb" } before(:all) do reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self'), style_src: %w('self') } end end after(:each) do Configuration.instance_variable_set(:@script_hashes, nil) Configuration.instance_variable_set(:@style_hashes, nil) end it "raises an error when using hashed content without precomputed hashes" do expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "raises an error when using hashed content with precomputed hashes, but none for the given file" do Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"]) expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"]) expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "adds known hash values to the corresponding headers when the helper is used" do begin allow(SecureRandom).to receive(:base64).and_return("abc123") expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"]) expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"]) # render erb that calls out to helpers. Message.new(request).result _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) end end it "avoids calling content_security_policy_nonce internally" do begin allow(SecureRandom).to receive(:base64).and_return("abc123") expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"]) expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"]) # render erb that calls out to helpers. MessageWithConflictingMethod.new(request).result _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).not_to match(/rails-nonce/) end end end end secure_headers-6.3.2/spec/lib/secure_headers_spec.rb000066400000000000000000000511041401055541100225200ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" module SecureHeaders describe SecureHeaders do before(:each) do reset_config end let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } it "raises a NotYetConfiguredError if default has not been set" do expect do SecureHeaders.header_hash_for(request) end.to raise_error(Configuration::NotYetConfiguredError) end it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do expect do SecureHeaders.opt_out_of_header(request, :csp) end.to raise_error(Configuration::NotYetConfiguredError) end it "raises a AlreadyConfiguredError if trying to configure and default has already been set " do Configuration.default expect do Configuration.default end.to raise_error(Configuration::AlreadyConfiguredError) end it "raises and ArgumentError when referencing an override that has not been set" do expect do Configuration.default SecureHeaders.use_secure_headers_override(request, :missing) end.to raise_error(ArgumentError) end describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self')} config.csp_report_only = config.csp end SecureHeaders.opt_out_of_header(request, :csp) SecureHeaders.opt_out_of_header(request, :csp_report_only) SecureHeaders.opt_out_of_header(request, :x_content_type_options) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy-Report-Only"]).to be_nil expect(hash["Content-Security-Policy"]).to be_nil expect(hash["X-Content-Type-Options"]).to be_nil end it "Carries options over when using overrides" do Configuration.default do |config| config.x_download_options = OPT_OUT config.x_permitted_cross_domain_policies = OPT_OUT end Configuration.override(:api) do |config| config.x_frame_options = OPT_OUT end SecureHeaders.use_secure_headers_override(request, :api) hash = SecureHeaders.header_hash_for(request) expect(hash["X-Download-Options"]).to be_nil expect(hash["X-Permitted-Cross-Domain-Policies"]).to be_nil expect(hash["X-Frame-Options"]).to be_nil end it "Overrides the current default config if default config changes during request" do Configuration.default do |config| config.x_frame_options = OPT_OUT end # Dynamically update the default config for this request SecureHeaders.override_x_frame_options(request, "DENY") Configuration.override(:dynamic_override) do |config| config.x_content_type_options = "nosniff" end SecureHeaders.use_secure_headers_override(request, :dynamic_override) hash = SecureHeaders.header_hash_for(request) expect(hash["X-Content-Type-Options"]).to eq("nosniff") expect(hash["X-Frame-Options"]).to eq("DENY") end it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| config.csp = { default_src: ["example.com"], script_src: %w('self') } config.csp_report_only = config.csp end SecureHeaders.opt_out_of_all_protection(request) hash = SecureHeaders.header_hash_for(request) expect(hash.count).to eq(0) end it "allows you to override X-Frame-Options settings" do Configuration.default SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY) hash = SecureHeaders.header_hash_for(request) expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY) end it "allows you to override opting out" do Configuration.default do |config| config.x_frame_options = OPT_OUT config.csp = OPT_OUT end SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN) SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self')) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'") expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end it "produces a hash of headers with default config" do Configuration.default hash = SecureHeaders.header_hash_for(request) expect_default_values(hash) end it "does not set the HSTS header if request is over HTTP" do plaintext_request = Rack::Request.new({}) Configuration.default do |config| config.hsts = "max-age=123456" end expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil end context "content security policy" do let(:chrome_request) { Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) } it "appends a value to csp directive" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com 'unsafe-inline') } end SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end it "supports named appends" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end Configuration.named_append(:moar_default_sources) do |request| { default_src: %w(https:), style_src: %w('self')} end Configuration.named_append(:how_about_a_script_src_too) do |request| { script_src: %w('unsafe-inline')} end SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources) SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' 'unsafe-inline'; style-src 'self'") end it "appends a nonce to a missing script-src value" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header hash = SecureHeaders.header_hash_for(chrome_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/) end it "appends a hash to a missing script-src value" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123')) hash = SecureHeaders.header_hash_for(chrome_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/) end it "overrides individual directives" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none')) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'; script-src 'self'") end it "overrides non-existant directives" do Configuration.default do |config| config.csp = { default_src: %w(https:), script_src: %w('self') } end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'") end it "appends a nonce to the script-src when used" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com), style_src: %w('self') } end nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request) # simulate the nonce being used multiple times in a request: SecureHeaders.content_security_policy_script_nonce(chrome_request) SecureHeaders.content_security_policy_script_nonce(chrome_request) SecureHeaders.content_security_policy_script_nonce(chrome_request) hash = SecureHeaders.header_hash_for(chrome_request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'") end it "does not support the deprecated `report_only: true` format" do expect { Configuration.default do |config| config.csp = { default_src: %w('self'), report_only: true } end }.to raise_error(ContentSecurityPolicyConfigError) end it "Raises an error if csp_report_only is used with `report_only: false`" do expect do Configuration.default do |config| config.csp_report_only = { default_src: %w('self'), script_src: %w('self'), report_only: false } end end.to raise_error(ContentSecurityPolicyConfigError) end context "setting two headers" do before(:each) do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } config.csp_report_only = config.csp end end it "sets identical values when the configs are the same" do reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } config.csp_report_only = { default_src: %w('self'), script_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "sets different headers when the configs are different" do reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } config.csp_report_only = config.csp.merge({script_src: %w(foo.com)}) end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src foo.com") end it "allows you to opt-out of enforced CSP" do reset_config Configuration.default do |config| config.csp = SecureHeaders::OPT_OUT config.csp_report_only = { default_src: %w('self'), script_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to be_nil expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows appending to the report only policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows appending to both policies" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows overriding the enforced policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self'") end it "allows overriding the report only policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self'") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") end it "allows overriding both policies" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src anothercdn.com") end context "when inferring which config to modify" do it "updates the enforced header when configured" do reset_config Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to be_nil end it "updates the report only header when configured" do reset_config Configuration.default do |config| config.csp = OPT_OUT config.csp_report_only = { default_src: %w('self'), script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash["Content-Security-Policy"]).to be_nil end it "updates both headers if both are configured" do reset_config Configuration.default do |config| config.csp = { default_src: %w(enforced.com), script_src: %w('self') } config.csp_report_only = { default_src: %w(reportonly.com), script_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash["Content-Security-Policy"]).to eq("default-src enforced.com; script-src 'self' anothercdn.com") expect(hash["Content-Security-Policy-Report-Only"]).to eq("default-src reportonly.com; script-src 'self' anothercdn.com") end end end end end context "validation" do it "validates your hsts config upon configuration" do expect do Configuration.default do |config| config.hsts = "lol" end end.to raise_error(STSConfigError) end it "validates your csp config upon configuration" do expect do Configuration.default do |config| config.csp = { ContentSecurityPolicy::DEFAULT_SRC => "123456" } end end.to raise_error(ContentSecurityPolicyConfigError) end it "raises errors for unknown directives" do expect do Configuration.default do |config| config.csp = { made_up_directive: "123456" } end end.to raise_error(ContentSecurityPolicyConfigError) end it "validates your xfo config upon configuration" do expect do Configuration.default do |config| config.x_frame_options = "NOPE" end end.to raise_error(XFOConfigError) end it "validates your xcto config upon configuration" do expect do Configuration.default do |config| config.x_content_type_options = "lol" end end.to raise_error(XContentTypeOptionsConfigError) end it "validates your clear site data config upon configuration" do expect do Configuration.default do |config| config.clear_site_data = 1 end end.to raise_error(ClearSiteDataConfigError) end it "validates your x_xss config upon configuration" do expect do Configuration.default do |config| config.x_xss_protection = "lol" end end.to raise_error(XXssProtectionConfigError) end it "validates your xdo config upon configuration" do expect do Configuration.default do |config| config.x_download_options = "lol" end end.to raise_error(XDOConfigError) end it "validates your x_permitted_cross_domain_policies config upon configuration" do expect do Configuration.default do |config| config.x_permitted_cross_domain_policies = "lol" end end.to raise_error(XPCDPConfigError) end it "validates your referrer_policy config upon configuration" do expect do Configuration.default do |config| config.referrer_policy = "lol" end end.to raise_error(ReferrerPolicyConfigError) end it "validates your cookies config upon configuration" do expect do Configuration.default do |config| config.cookies = { secure: "lol" } end end.to raise_error(CookiesConfigError) end end end end secure_headers-6.3.2/spec/spec_helper.rb000066400000000000000000000070631401055541100202550ustar00rootroot00000000000000# frozen_string_literal: true require "rubygems" require "rspec" require "rack" require "coveralls" Coveralls.wear! require File.join(File.dirname(__FILE__), "..", "lib", "secure_headers") USER_AGENTS = { edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1", firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0", chrome: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5", ie: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", opera: "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00", ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1", safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11" } def expect_default_values(hash) expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") expect(hash[SecureHeaders::ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE) expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE) expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ClearSiteData::HEADER_NAME]).to be_nil expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil end module SecureHeaders class Configuration class << self def clear_default_config remove_instance_variable(:@default_config) if defined?(@default_config) end def clear_overrides remove_instance_variable(:@overrides) if defined?(@overrides) end def clear_appends remove_instance_variable(:@appends) if defined?(@appends) end end end end def reset_config SecureHeaders::Configuration.clear_default_config SecureHeaders::Configuration.clear_overrides SecureHeaders::Configuration.clear_appends end