pax_global_header 0000666 0000000 0000000 00000000064 13504726733 0014524 g ustar 00root root 0000000 0000000 52 comment=92ac88736ac3f3aad0d6401adb3cc954a1a2989a
secure_headers-6.1.1/ 0000775 0000000 0000000 00000000000 13504726733 0014512 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/.github/ 0000775 0000000 0000000 00000000000 13504726733 0016052 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/.github/ISSUE_TEMPLATE.md 0000664 0000000 0000000 00000002053 13504726733 0020557 0 ustar 00root root 0000000 0000000 # 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.1.1/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000000700 13504726733 0021650 0 ustar 00root root 0000000 0000000 ## 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.1.1/.gitignore 0000664 0000000 0000000 00000000151 13504726733 0016477 0 ustar 00root root 0000000 0000000 *.gem
*.DS_STORE
*.rbc
.bundle
.config
.yardoc
*.log
Gemfile.lock
_yardoc
coverage
pkg
rdoc
spec/reports
secure_headers-6.1.1/.rspec 0000664 0000000 0000000 00000000052 13504726733 0015624 0 ustar 00root root 0000000 0000000 --order rand
--warnings
--format progress
secure_headers-6.1.1/.rubocop.yml 0000664 0000000 0000000 00000000070 13504726733 0016761 0 ustar 00root root 0000000 0000000 inherit_gem:
rubocop-github:
- config/default.yml
secure_headers-6.1.1/.ruby-gemset 0000664 0000000 0000000 00000000016 13504726733 0016753 0 ustar 00root root 0000000 0000000 secureheaders
secure_headers-6.1.1/.ruby-version 0000664 0000000 0000000 00000000006 13504726733 0017153 0 ustar 00root root 0000000 0000000 2.6.1
secure_headers-6.1.1/.travis.yml 0000664 0000000 0000000 00000000560 13504726733 0016624 0 ustar 00root root 0000000 0000000 language: ruby
rvm:
- ruby-head
- 2.6.1
- 2.5.0
- 2.4.3
- jruby-head
env:
- SUITE=rspec spec
- SUITE=rubocop
script: bundle exec $SUITE
matrix:
allow_failures:
- rvm: jruby-head
- rvm: ruby-head
before_install:
- gem update --system
- gem --version
- gem update bundler
bundler_args: --without guard -j 3
sudo: false
cache: bundler
secure_headers-6.1.1/CHANGELOG.md 0000664 0000000 0000000 00000055046 13504726733 0016335 0 ustar 00root root 0000000 0000000 ## 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/master/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.1.1/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006224 13504726733 0017315 0 ustar 00root root 0000000 0000000 # 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.1.1/CONTRIBUTING.md 0000664 0000000 0000000 00000003536 13504726733 0016752 0 ustar 00root root 0000000 0000000 ## 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.1.1/Gemfile 0000664 0000000 0000000 00000000641 13504726733 0016006 0 ustar 00root root 0000000 0000000 # 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"
gem "rubocop-github"
gem "term-ansicolor"
gem "tins"
end
group :guard do
gem "growl"
gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24]
gem "rb-fsevent"
gem "terminal-notifier-guard"
end
secure_headers-6.1.1/Guardfile 0000664 0000000 0000000 00000000622 13504726733 0016337 0 ustar 00root root 0000000 0000000 # 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.1.1/LICENSE 0000664 0000000 0000000 00000002065 13504726733 0015522 0 ustar 00root root 0000000 0000000 Copyright 2013, 2014, 2015, 2016, 2017 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.1.1/README.md 0000664 0000000 0000000 00000017727 13504726733 0016007 0 ustar 00root root 0000000 0000000 # Secure Headers [](http://travis-ci.org/twitter/secureheaders) [](https://codeclimate.com/github/twitter/secureheaders) [](https://coveralls.io/r/twitter/secureheaders)
**master 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.
# 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'),
style_src: %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.
## 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)
* J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines)
* 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)
## License
Copyright 2013-2014 Twitter, Inc and other contributors.
Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
secure_headers-6.1.1/Rakefile 0000664 0000000 0000000 00000001206 13504726733 0016156 0 ustar 00root root 0000000 0000000 #!/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.1.1/docs/ 0000775 0000000 0000000 00000000000 13504726733 0015442 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/docs/cookies.md 0000664 0000000 0000000 00000003275 13504726733 0017427 0 ustar 00root root 0000000 0000000 ## 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: 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` and `Lax` enforcement modes can also be specified using a Hash.
```ruby
config.cookies = {
samesite: {
strict: { only: ['_rails_session'] },
lax: { only: ['_guest'] }
}
}
```
secure_headers-6.1.1/docs/hashes.md 0000664 0000000 0000000 00000004444 13504726733 0017245 0 ustar 00root root 0000000 0000000 ## 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.1.1/docs/named_overrides_and_appends.md 0000664 0000000 0000000 00000007116 13504726733 0023473 0 ustar 00root root 0000000 0000000 ## 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|
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
# overrides the :script_from_otherdomain_com configuration
SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
config.csp[:script_src] << "evenanotherdomain.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
```
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.1.1/docs/per_action_configuration.md 0000664 0000000 0000000 00000011114 13504726733 0023034 0 ustar 00root root 0000000 0000000 ## 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.1.1/docs/sinatra.md 0000664 0000000 0000000 00000000660 13504726733 0017427 0 ustar 00root root 0000000 0000000 ## 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.1.1/docs/upgrading-to-3-0.md 0000664 0000000 0000000 00000016415 13504726733 0020670 0 ustar 00root root 0000000 0000000 `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.1.1/docs/upgrading-to-4-0.md 0000664 0000000 0000000 00000002641 13504726733 0020665 0 ustar 00root root 0000000 0000000 ## 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.1.1/docs/upgrading-to-5-0.md 0000664 0000000 0000000 00000001051 13504726733 0020660 0 ustar 00root root 0000000 0000000 ## 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.1.1/docs/upgrading-to-6-0.md 0000664 0000000 0000000 00000007564 13504726733 0020700 0 ustar 00root root 0000000 0000000 ## 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 = 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.
## Configuration 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 inc 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.1.1/lib/ 0000775 0000000 0000000 00000000000 13504726733 0015260 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/lib/secure_headers.rb 0000664 0000000 0000000 00000022207 13504726733 0020571 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/ 0000775 0000000 0000000 00000000000 13504726733 0020241 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/lib/secure_headers/configuration.rb 0000664 0000000 0000000 00000020554 13504726733 0023443 0 ustar 00root root 0000000 0000000 # 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?
@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?
@appends[name] = block
end
def dup
default_config.dup
end
private
# 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_accessor(*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.1.1/lib/secure_headers/hash_helper.rb 0000664 0000000 0000000 00000000477 13504726733 0023060 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/ 0000775 0000000 0000000 00000000000 13504726733 0021654 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/lib/secure_headers/headers/clear_site_data.rb 0000664 0000000 0000000 00000003174 13504726733 0025311 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/content_security_policy.rb 0000664 0000000 0000000 00000014661 13504726733 0027171 0 ustar 00root root 0000000 0000000 # 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?
normalized_source_list = minify_source_list(directive, source_list)
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
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.1.1/lib/secure_headers/headers/content_security_policy_config.rb 0000664 0000000 0000000 00000007263 13504726733 0030516 0 ustar 00root root 0000000 0000000 # 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
@style_nonce = nil
@style_src = 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.1.1/lib/secure_headers/headers/cookie.rb 0000664 0000000 0000000 00000006045 13504726733 0023457 0 ustar 00root root 0000000 0000000 # 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"
end
end
def flag_samesite?
return false if config == OPT_OUT || config[:samesite] == OPT_OUT
flag_samesite_lax? || flag_samesite_strict?
end
def flag_samesite_lax?
flag_samesite_enforcement?(:lax)
end
def flag_samesite_strict?
flag_samesite_enforcement?(:strict)
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.1.1/lib/secure_headers/headers/expect_certificate_transparency.rb 0000664 0000000 0000000 00000004246 13504726733 0030632 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/policy_management.rb 0000664 0000000 0000000 00000034527 13504726733 0025707 0 ustar 00root root 0000000 0000000 # 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
DIRECTIVES_3_0 = [
DIRECTIVES_2_0,
BLOCK_ALL_MIXED_CONTENT,
MANIFEST_SRC,
NAVIGATE_TO,
PREFETCH_SRC,
REQUIRE_SRI_FOR,
WORKER_SRC,
UPGRADE_INSECURE_REQUESTS
].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,
STYLE_SRC => :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.1.1/lib/secure_headers/headers/referrer_policy.rb 0000664 0000000 0000000 00000002514 13504726733 0025376 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/strict_transport_security.rb 0000664 0000000 0000000 00000002127 13504726733 0027556 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/x_content_type_options.rb 0000664 0000000 0000000 00000001667 13504726733 0027030 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/x_download_options.rb 0000664 0000000 0000000 00000001611 13504726733 0026111 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/x_frame_options.rb 0000664 0000000 0000000 00000002116 13504726733 0025375 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb 0000664 0000000 0000000 00000002036 13504726733 0031155 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/headers/x_xss_protection.rb 0000664 0000000 0000000 00000001720 13504726733 0025613 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/middleware.rb 0000664 0000000 0000000 00000003077 13504726733 0022712 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/railtie.rb 0000664 0000000 0000000 00000002642 13504726733 0022223 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/secure_headers/utils/ 0000775 0000000 0000000 00000000000 13504726733 0021401 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/lib/secure_headers/utils/cookies_config.rb 0000664 0000000 0000000 00000007230 13504726733 0024711 0 ustar 00root root 0000000 0000000 # 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)
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement 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.1.1/lib/secure_headers/version.rb 0000664 0000000 0000000 00000000114 13504726733 0022247 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module SecureHeaders
VERSION = "6.1.1"
end
secure_headers-6.1.1/lib/secure_headers/view_helper.rb 0000664 0000000 0000000 00000013546 13504726733 0023110 0 ustar 00root root 0000000 0000000 # 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.1.1/lib/tasks/ 0000775 0000000 0000000 00000000000 13504726733 0016405 5 ustar 00root root 0000000 0000000 secure_headers-6.1.1/lib/tasks/tasks.rake 0000664 0000000 0000000 00000005123 13504726733 0020377 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
INLINE_SCRIPT_REGEX = /(
<%= 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}#{type}>"
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.1.1/spec/lib/secure_headers_spec.rb 0000664 0000000 0000000 00000051104 13504726733 0022533 0 ustar 00root root 0000000 0000000 # 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.1.1/spec/spec_helper.rb 0000664 0000000 0000000 00000006407 13504726733 0020271 0 ustar 00root root 0000000 0000000 # 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
end
end
end
def reset_config
SecureHeaders::Configuration.clear_default_config
end