secure_headers 3.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +0 -9
- data/.travis.yml +13 -5
- data/CHANGELOG.md +11 -0
- data/Gemfile +5 -2
- data/README.md +7 -42
- data/lib/secure_headers.rb +37 -63
- data/lib/secure_headers/configuration.rb +85 -54
- data/lib/secure_headers/headers/content_security_policy.rb +31 -309
- data/lib/secure_headers/headers/policy_management.rb +319 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/middleware.rb +23 -0
- data/lib/secure_headers/railtie.rb +1 -1
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +2 -4
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -175
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +190 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +1 -1
- data/spec/lib/secure_headers/middleware_spec.rb +23 -4
- data/spec/lib/secure_headers_spec.rb +100 -41
- data/spec/spec_helper.rb +4 -1
- metadata +5 -4
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 7353ab86b76cba9baabc265f6321990163927fa0
         | 
| 4 | 
            +
              data.tar.gz: 8081e9d2a284a26191899317d1947cde24d80d9f
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 37fd3a8aca07d1ce46bd7cdaf89ddb057d41b94f2f41625287d9a3118d19300708fd33f9d5f029c341a9c80295848101f01558d3dfd370a7bba34c8862e710d6
         | 
| 7 | 
            +
              data.tar.gz: 0d810d24a48f53a45bec5621291475059e521d0d4f640bf1430ad6c86ecf020fea8dd8eb220e600b71c32539d7cfbc1f87567066477deebdf660d8a0161e2f8a
         | 
    
        data/.gitignore
    CHANGED
    
    | @@ -6,17 +6,8 @@ | |
| 6 6 | 
             
            .yardoc
         | 
| 7 7 | 
             
            *.log
         | 
| 8 8 | 
             
            Gemfile.lock
         | 
| 9 | 
            -
            InstalledFiles
         | 
| 10 9 | 
             
            _yardoc
         | 
| 11 10 | 
             
            coverage
         | 
| 12 | 
            -
            doc/
         | 
| 13 | 
            -
            lib/bundler/man
         | 
| 14 11 | 
             
            pkg
         | 
| 15 12 | 
             
            rdoc
         | 
| 16 13 | 
             
            spec/reports
         | 
| 17 | 
            -
            test/tmp
         | 
| 18 | 
            -
            test/version_tmp
         | 
| 19 | 
            -
            *tmp
         | 
| 20 | 
            -
            *.sqlite3
         | 
| 21 | 
            -
            fixtures/rails_3_2_12_no_init/log
         | 
| 22 | 
            -
            fixtures/rails_3_2_12/log
         | 
    
        data/.travis.yml
    CHANGED
    
    | @@ -1,13 +1,21 @@ | |
| 1 1 | 
             
            language: ruby
         | 
| 2 2 |  | 
| 3 3 | 
             
            rvm:
         | 
| 4 | 
            -
              -  | 
| 5 | 
            -
              -  | 
| 6 | 
            -
              -  | 
| 7 | 
            -
              -  | 
| 8 | 
            -
              -  | 
| 4 | 
            +
              - ruby-head
         | 
| 5 | 
            +
              - 2.2
         | 
| 6 | 
            +
              - 2.1
         | 
| 7 | 
            +
              - 2.0.0
         | 
| 8 | 
            +
              - 1.9.3
         | 
| 9 | 
            +
              - jruby-19mode
         | 
| 10 | 
            +
              - jruby-head
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            matrix:
         | 
| 13 | 
            +
              allow_failures:
         | 
| 14 | 
            +
                - rvm: jruby-head
         | 
| 15 | 
            +
                - rvm: ruby-head
         | 
| 9 16 |  | 
| 10 17 | 
             
            before_install: gem update bundler
         | 
| 18 | 
            +
            bundler_args: --without guard -j 3
         | 
| 11 19 |  | 
| 12 20 | 
             
            sudo: false
         | 
| 13 21 | 
             
            cache: bundler
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,13 @@ | |
| 1 | 
            +
            ## 3.1.0 Adding secure cookie support
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            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.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            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:
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            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.
         | 
| 8 | 
            +
            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.
         | 
| 9 | 
            +
            1. CSP header string is produced at most once per request.
         | 
| 10 | 
            +
             | 
| 1 11 | 
             
            ## 3.0.3
         | 
| 2 12 |  | 
| 3 13 | 
             
            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:
         | 
| @@ -14,6 +24,7 @@ When appending: | |
| 14 24 | 
             
            {
         | 
| 15 25 | 
             
              report_uri => %w(https://report-uri.io/asdf)
         | 
| 16 26 | 
             
            }
         | 
| 27 | 
            +
            ```
         | 
| 17 28 |  | 
| 18 29 | 
             
            Would result in `default-src *; report-uri *` which doesn't make any sense at all.
         | 
| 19 30 |  | 
    
        data/Gemfile
    CHANGED
    
    | @@ -6,9 +6,12 @@ group :test do | |
| 6 6 | 
             
              gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0
         | 
| 7 7 | 
             
              gem "pry-nav"
         | 
| 8 8 | 
             
              gem "rack"
         | 
| 9 | 
            +
              gem "rspec"
         | 
| 10 | 
            +
              gem "coveralls"
         | 
| 11 | 
            +
            end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            group :guard do
         | 
| 9 14 | 
             
              gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22]
         | 
| 10 | 
            -
              gem "rspec", ">= 3.1"
         | 
| 11 15 | 
             
              gem "growl"
         | 
| 12 16 | 
             
              gem "rb-fsevent"
         | 
| 13 | 
            -
              gem "coveralls", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22]
         | 
| 14 17 | 
             
            end
         | 
    
        data/README.md
    CHANGED
    
    | @@ -29,7 +29,7 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` | |
| 29 29 |  | 
| 30 30 | 
             
            ```ruby
         | 
| 31 31 | 
             
            SecureHeaders::Configuration.default do |config|
         | 
| 32 | 
            -
              config.hsts = "max-age=#{20.years.to_i}"
         | 
| 32 | 
            +
              config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
         | 
| 33 33 | 
             
              config.x_frame_options = "DENY"
         | 
| 34 34 | 
             
              config.x_content_type_options = "nosniff"
         | 
| 35 35 | 
             
              config.x_xss_protection = "1; mode=block"
         | 
| @@ -57,13 +57,13 @@ SecureHeaders::Configuration.default do |config| | |
| 57 57 | 
             
                plugin_types: %w(application/x-shockwave-flash),
         | 
| 58 58 | 
             
                block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
         | 
| 59 59 | 
             
                upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
         | 
| 60 | 
            -
                report_uri: %w(https:// | 
| 60 | 
            +
                report_uri: %w(https://report-uri.io/example-csp)
         | 
| 61 61 | 
             
              }
         | 
| 62 62 | 
             
              config.hpkp = {
         | 
| 63 63 | 
             
                report_only: false,
         | 
| 64 64 | 
             
                max_age: 60.days.to_i,
         | 
| 65 65 | 
             
                include_subdomains: true,
         | 
| 66 | 
            -
                report_uri: "https:// | 
| 66 | 
            +
                report_uri: "https://report-uri.io/example-hpkp",
         | 
| 67 67 | 
             
                pins: [
         | 
| 68 68 | 
             
                  {sha256: "abc"},
         | 
| 69 69 | 
             
                  {sha256: "123"}
         | 
| @@ -175,7 +175,7 @@ When manipulating content security policy, there are a few things to consider. T | |
| 175 175 |  | 
| 176 176 | 
             
            #### Append to the policy with a directive other than `default_src`
         | 
| 177 177 |  | 
| 178 | 
            -
            The value of `default_src` is joined with the addition. 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:
         | 
| 178 | 
            +
            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:
         | 
| 179 179 |  | 
| 180 180 | 
             
            ```ruby
         | 
| 181 181 | 
             
            ::SecureHeaders::Configuration.default do |config|
         | 
| @@ -255,7 +255,7 @@ config.hpkp = { | |
| 255 255 | 
             
                {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
         | 
| 256 256 | 
             
              ],
         | 
| 257 257 | 
             
              report_only: true,            # defaults to false (report-only mode)
         | 
| 258 | 
            -
              report_uri: ' | 
| 258 | 
            +
              report_uri: 'https://report-uri.io/example-hpkp',
         | 
| 259 259 | 
             
              app_name: 'example',
         | 
| 260 260 | 
             
              tag_report_uri: true
         | 
| 261 261 | 
             
            }
         | 
| @@ -287,43 +287,6 @@ class Donkey < Sinatra::Application | |
| 287 287 | 
             
            end
         | 
| 288 288 | 
             
            ```
         | 
| 289 289 |  | 
| 290 | 
            -
            ### Using with Padrino
         | 
| 291 | 
            -
             | 
| 292 | 
            -
            You can use SecureHeaders for Padrino applications as well:
         | 
| 293 | 
            -
             | 
| 294 | 
            -
            In your `Gemfile`:
         | 
| 295 | 
            -
             | 
| 296 | 
            -
            ```ruby
         | 
| 297 | 
            -
              gem "secure_headers", require: 'secure_headers'
         | 
| 298 | 
            -
            ```
         | 
| 299 | 
            -
             | 
| 300 | 
            -
            then in your `app.rb` file you can:
         | 
| 301 | 
            -
             | 
| 302 | 
            -
            ```ruby
         | 
| 303 | 
            -
            Padrino.use(SecureHeaders::Middleware)
         | 
| 304 | 
            -
            require 'secure_headers/padrino'
         | 
| 305 | 
            -
             | 
| 306 | 
            -
            module Web
         | 
| 307 | 
            -
              class App < Padrino::Application
         | 
| 308 | 
            -
                register SecureHeaders::Padrino
         | 
| 309 | 
            -
             | 
| 310 | 
            -
                get '/' do
         | 
| 311 | 
            -
                  render 'index'
         | 
| 312 | 
            -
                end
         | 
| 313 | 
            -
              end
         | 
| 314 | 
            -
            end
         | 
| 315 | 
            -
            ```
         | 
| 316 | 
            -
             | 
| 317 | 
            -
            and in `config/boot.rb`:
         | 
| 318 | 
            -
             | 
| 319 | 
            -
            ```ruby
         | 
| 320 | 
            -
            def before_load
         | 
| 321 | 
            -
              SecureHeaders::Configuration.default do |config|
         | 
| 322 | 
            -
                ...
         | 
| 323 | 
            -
              end
         | 
| 324 | 
            -
            end
         | 
| 325 | 
            -
            ```
         | 
| 326 | 
            -
             | 
| 327 290 | 
             
            ## Similar libraries
         | 
| 328 291 |  | 
| 329 292 | 
             
            * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers)
         | 
| @@ -334,6 +297,8 @@ end | |
| 334 297 | 
             
            * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security)
         | 
| 335 298 | 
             
            * Go - [secureheader](https://github.com/kr/secureheader)
         | 
| 336 299 | 
             
            * Elixir [secure_headers](https://github.com/anotherhale/secure_headers)
         | 
| 300 | 
            +
            * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security)
         | 
| 301 | 
            +
            * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/)
         | 
| 337 302 |  | 
| 338 303 | 
             
            ## License
         | 
| 339 304 |  | 
    
        data/lib/secure_headers.rb
    CHANGED
    
    | @@ -48,14 +48,12 @@ module SecureHeaders | |
| 48 48 | 
             
                #    script_src: %w(another-host.com)
         | 
| 49 49 | 
             
                def override_content_security_policy_directives(request, additions)
         | 
| 50 50 | 
             
                  config = config_for(request)
         | 
| 51 | 
            -
                   | 
| 52 | 
            -
                    config =  | 
| 53 | 
            -
                    if config.csp == OPT_OUT
         | 
| 54 | 
            -
                      config.csp = {}
         | 
| 55 | 
            -
                    end
         | 
| 56 | 
            -
                    config.csp.merge!(additions)
         | 
| 57 | 
            -
                    override_secure_headers_request_config(request, config)
         | 
| 51 | 
            +
                  if config.current_csp == OPT_OUT
         | 
| 52 | 
            +
                    config.dynamic_csp = {}
         | 
| 58 53 | 
             
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  config.dynamic_csp = config.current_csp.merge(additions)
         | 
| 56 | 
            +
                  override_secure_headers_request_config(request, config)
         | 
| 59 57 | 
             
                end
         | 
| 60 58 |  | 
| 61 59 | 
             
                # Public: appends source values to the current configuration. If no value
         | 
| @@ -66,11 +64,8 @@ module SecureHeaders | |
| 66 64 | 
             
                #    script_src: %w(another-host.com)
         | 
| 67 65 | 
             
                def append_content_security_policy_directives(request, additions)
         | 
| 68 66 | 
             
                  config = config_for(request)
         | 
| 69 | 
            -
                   | 
| 70 | 
            -
             | 
| 71 | 
            -
                    config.csp = CSP.combine_policies(config.csp, additions)
         | 
| 72 | 
            -
                    override_secure_headers_request_config(request, config)
         | 
| 73 | 
            -
                  end
         | 
| 67 | 
            +
                  config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
         | 
| 68 | 
            +
                  override_secure_headers_request_config(request, config)
         | 
| 74 69 | 
             
                end
         | 
| 75 70 |  | 
| 76 71 | 
             
                # Public: override X-Frame-Options settings for this request.
         | 
| @@ -79,16 +74,16 @@ module SecureHeaders | |
| 79 74 | 
             
                #
         | 
| 80 75 | 
             
                # Returns the current config
         | 
| 81 76 | 
             
                def override_x_frame_options(request, value)
         | 
| 82 | 
            -
                   | 
| 83 | 
            -
                   | 
| 84 | 
            -
                  override_secure_headers_request_config(request,  | 
| 77 | 
            +
                  config = config_for(request)
         | 
| 78 | 
            +
                  config.update_x_frame_options(value)
         | 
| 79 | 
            +
                  override_secure_headers_request_config(request, config)
         | 
| 85 80 | 
             
                end
         | 
| 86 81 |  | 
| 87 82 | 
             
                # Public: opts out of setting a given header by creating a temporary config
         | 
| 88 83 | 
             
                # and setting the given headers config to OPT_OUT.
         | 
| 89 84 | 
             
                def opt_out_of_header(request, header_key)
         | 
| 90 | 
            -
                  config = config_for(request) | 
| 91 | 
            -
                  config. | 
| 85 | 
            +
                  config = config_for(request)
         | 
| 86 | 
            +
                  config.opt_out(header_key)
         | 
| 92 87 | 
             
                  override_secure_headers_request_config(request, config)
         | 
| 93 88 | 
             
                end
         | 
| 94 89 |  | 
| @@ -109,14 +104,11 @@ module SecureHeaders | |
| 109 104 | 
             
                # in Rack middleware.
         | 
| 110 105 | 
             
                def header_hash_for(request)
         | 
| 111 106 | 
             
                  config = config_for(request)
         | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
                    use_cached_headers(cached_headers, request)
         | 
| 115 | 
            -
                  else
         | 
| 116 | 
            -
                    build_headers(config, request)
         | 
| 107 | 
            +
                  unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
         | 
| 108 | 
            +
                    config.rebuild_csp_header_cache!(request.user_agent)
         | 
| 117 109 | 
             
                  end
         | 
| 118 110 |  | 
| 119 | 
            -
                   | 
| 111 | 
            +
                  use_cached_headers(config.cached_headers, request)
         | 
| 120 112 | 
             
                end
         | 
| 121 113 |  | 
| 122 114 | 
             
                # Public: specify which named override will be used for this request.
         | 
| @@ -149,6 +141,22 @@ module SecureHeaders | |
| 149 141 | 
             
                  content_security_policy_nonce(request, CSP::STYLE_SRC)
         | 
| 150 142 | 
             
                end
         | 
| 151 143 |  | 
| 144 | 
            +
                # Public: Retreives the config for a given header type:
         | 
| 145 | 
            +
                #
         | 
| 146 | 
            +
                # Checks to see if there is an override for this request, then
         | 
| 147 | 
            +
                # Checks to see if a named override is used for this request, then
         | 
| 148 | 
            +
                # Falls back to the global config
         | 
| 149 | 
            +
                def config_for(request)
         | 
| 150 | 
            +
                  config = request.env[SECURE_HEADERS_CONFIG] ||
         | 
| 151 | 
            +
                    Configuration.get(Configuration::DEFAULT_CONFIG)
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  if config.frozen?
         | 
| 154 | 
            +
                    config.dup
         | 
| 155 | 
            +
                  else
         | 
| 156 | 
            +
                    config
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 152 160 | 
             
                private
         | 
| 153 161 |  | 
| 154 162 | 
             
                # Private: gets or creates a nonce for CSP.
         | 
| @@ -181,64 +189,30 @@ module SecureHeaders | |
| 181 189 | 
             
                  end
         | 
| 182 190 | 
             
                end
         | 
| 183 191 |  | 
| 184 | 
            -
                # Private: do the heavy lifting of converting a configuration object
         | 
| 185 | 
            -
                # to a hash of headers valid for this request.
         | 
| 186 | 
            -
                #
         | 
| 187 | 
            -
                # Returns a hash of header names / values.
         | 
| 188 | 
            -
                def build_headers(config, request)
         | 
| 189 | 
            -
                  header_classes_for(request).each_with_object({}) do |klass, hash|
         | 
| 190 | 
            -
                    header_config = if config
         | 
| 191 | 
            -
                      config.fetch(klass::CONFIG_KEY)
         | 
| 192 | 
            -
                    end
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                    header_name, value = if klass == CSP
         | 
| 195 | 
            -
                      make_header(klass, header_config, request.user_agent)
         | 
| 196 | 
            -
                    else
         | 
| 197 | 
            -
                      make_header(klass, header_config)
         | 
| 198 | 
            -
                    end
         | 
| 199 | 
            -
                    hash[header_name] = value if value
         | 
| 200 | 
            -
                  end
         | 
| 201 | 
            -
                end
         | 
| 202 | 
            -
             | 
| 203 192 | 
             
                # Private: takes a precomputed hash of headers and returns the Headers
         | 
| 204 193 | 
             
                # customized for the request.
         | 
| 205 194 | 
             
                #
         | 
| 206 195 | 
             
                # Returns a hash of header names / values valid for a given request.
         | 
| 207 | 
            -
                def use_cached_headers( | 
| 196 | 
            +
                def use_cached_headers(headers, request)
         | 
| 208 197 | 
             
                  header_classes_for(request).each_with_object({}) do |klass, hash|
         | 
| 209 | 
            -
                    if  | 
| 198 | 
            +
                    if header = headers[klass::CONFIG_KEY]
         | 
| 210 199 | 
             
                      header_name, value = if klass == CSP
         | 
| 211 | 
            -
                         | 
| 200 | 
            +
                        csp_header_for_ua(header, request)
         | 
| 212 201 | 
             
                      else
         | 
| 213 | 
            -
                         | 
| 202 | 
            +
                        header
         | 
| 214 203 | 
             
                      end
         | 
| 215 204 | 
             
                      hash[header_name] = value
         | 
| 216 205 | 
             
                    end
         | 
| 217 206 | 
             
                  end
         | 
| 218 207 | 
             
                end
         | 
| 219 208 |  | 
| 220 | 
            -
                # Private: Retreives the config for a given header type:
         | 
| 221 | 
            -
                #
         | 
| 222 | 
            -
                # Checks to see if there is an override for this request, then
         | 
| 223 | 
            -
                # Checks to see if a named override is used for this request, then
         | 
| 224 | 
            -
                # Falls back to the global config
         | 
| 225 | 
            -
                def config_for(request)
         | 
| 226 | 
            -
                  request.env[SECURE_HEADERS_CONFIG] ||
         | 
| 227 | 
            -
                    Configuration.get(Configuration::DEFAULT_CONFIG)
         | 
| 228 | 
            -
                end
         | 
| 229 | 
            -
             | 
| 230 209 | 
             
                # Private: chooses the applicable CSP header for the provided user agent.
         | 
| 231 210 | 
             
                #
         | 
| 232 211 | 
             
                # headers - a hash of header_config_key => [header_name, header_value]
         | 
| 233 212 | 
             
                #
         | 
| 234 213 | 
             
                # Returns a CSP [header, value] array
         | 
| 235 | 
            -
                def  | 
| 236 | 
            -
                   | 
| 237 | 
            -
                  if CSP::VARIATIONS.key?(family)
         | 
| 238 | 
            -
                    headers[family]
         | 
| 239 | 
            -
                  else
         | 
| 240 | 
            -
                    headers[CSP::OTHER]
         | 
| 241 | 
            -
                  end
         | 
| 214 | 
            +
                def csp_header_for_ua(headers, request)
         | 
| 215 | 
            +
                  headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
         | 
| 242 216 | 
             
                end
         | 
| 243 217 |  | 
| 244 218 | 
             
                # Private: optionally build a header with a given configure
         | 
| @@ -3,6 +3,7 @@ module SecureHeaders | |
| 3 3 | 
             
                DEFAULT_CONFIG = :default
         | 
| 4 4 | 
             
                NOOP_CONFIGURATION = "secure_headers_noop_config"
         | 
| 5 5 | 
             
                class NotYetConfiguredError < StandardError; end
         | 
| 6 | 
            +
                class IllegalPolicyModificationError < StandardError; end
         | 
| 6 7 | 
             
                class << self
         | 
| 7 8 | 
             
                  # Public: Set the global default configuration.
         | 
| 8 9 | 
             
                  #
         | 
| @@ -23,12 +24,12 @@ module SecureHeaders | |
| 23 24 | 
             
                  # if no value is supplied.
         | 
| 24 25 | 
             
                  #
         | 
| 25 26 | 
             
                  # Returns: the newly created config
         | 
| 26 | 
            -
                  def override(name, base = DEFAULT_CONFIG)
         | 
| 27 | 
            +
                  def override(name, base = DEFAULT_CONFIG, &block)
         | 
| 27 28 | 
             
                    unless get(base)
         | 
| 28 29 | 
             
                      raise NotYetConfiguredError, "#{base} policy not yet supplied"
         | 
| 29 30 | 
             
                    end
         | 
| 30 31 | 
             
                    override = @configurations[base].dup
         | 
| 31 | 
            -
                     | 
| 32 | 
            +
                    override.instance_eval &block if block_given?
         | 
| 32 33 | 
             
                    add_configuration(name, override)
         | 
| 33 34 | 
             
                  end
         | 
| 34 35 |  | 
| @@ -43,18 +44,6 @@ module SecureHeaders | |
| 43 44 | 
             
                    @configurations[name]
         | 
| 44 45 | 
             
                  end
         | 
| 45 46 |  | 
| 46 | 
            -
                  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
         | 
| 47 | 
            -
                  # can lead to modifying parent objects.
         | 
| 48 | 
            -
                  def deep_copy(config)
         | 
| 49 | 
            -
                    config.each_with_object({}) do |(key, value), hash|
         | 
| 50 | 
            -
                      hash[key] = if value.is_a?(Array)
         | 
| 51 | 
            -
                        value.dup
         | 
| 52 | 
            -
                      else
         | 
| 53 | 
            -
                        value
         | 
| 54 | 
            -
                      end
         | 
| 55 | 
            -
                    end
         | 
| 56 | 
            -
                  end
         | 
| 57 | 
            -
             | 
| 58 47 | 
             
                  private
         | 
| 59 48 |  | 
| 60 49 | 
             
                  # Private: add a valid configuration to the global set of named configs.
         | 
| @@ -86,16 +75,39 @@ module SecureHeaders | |
| 86 75 |  | 
| 87 76 | 
             
                    add_configuration(NOOP_CONFIGURATION, noop_config)
         | 
| 88 77 | 
             
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
         | 
| 80 | 
            +
                  # can lead to modifying parent objects.
         | 
| 81 | 
            +
                  def deep_copy(config)
         | 
| 82 | 
            +
                    config.each_with_object({}) do |(key, value), hash|
         | 
| 83 | 
            +
                      hash[key] = if value.is_a?(Array)
         | 
| 84 | 
            +
                        value.dup
         | 
| 85 | 
            +
                      else
         | 
| 86 | 
            +
                        value
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  # Private: convenience method purely DRY things up. The value may not be a
         | 
| 92 | 
            +
                  # hash (e.g. OPT_OUT, nil)
         | 
| 93 | 
            +
                  def deep_copy_if_hash(value)
         | 
| 94 | 
            +
                    if value.is_a?(Hash)
         | 
| 95 | 
            +
                      deep_copy(value)
         | 
| 96 | 
            +
                    else
         | 
| 97 | 
            +
                      value
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  end
         | 
| 89 100 | 
             
                end
         | 
| 90 101 |  | 
| 91 | 
            -
                 | 
| 92 | 
            -
                  :x_xss_protection, : | 
| 93 | 
            -
                  :hpkp
         | 
| 94 | 
            -
             | 
| 102 | 
            +
                attr_writer :hsts, :x_frame_options, :x_content_type_options,
         | 
| 103 | 
            +
                  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
         | 
| 104 | 
            +
                  :hpkp, :dynamic_csp, :secure_cookies
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies
         | 
| 95 107 |  | 
| 96 108 | 
             
                def initialize(&block)
         | 
| 97 109 | 
             
                  self.hpkp = OPT_OUT
         | 
| 98 | 
            -
                  self.csp = self.class.deep_copy | 
| 110 | 
            +
                  self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
         | 
| 99 111 | 
             
                  instance_eval &block if block_given?
         | 
| 100 112 | 
             
                end
         | 
| 101 113 |  | 
| @@ -104,33 +116,37 @@ module SecureHeaders | |
| 104 116 | 
             
                # Returns a deep-dup'd copy of this configuration.
         | 
| 105 117 | 
             
                def dup
         | 
| 106 118 | 
             
                  copy = self.class.new
         | 
| 107 | 
            -
                  copy. | 
| 108 | 
            -
                  copy. | 
| 109 | 
            -
                  copy. | 
| 110 | 
            -
                  copy. | 
| 111 | 
            -
                  copy | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
                   | 
| 116 | 
            -
             | 
| 119 | 
            +
                  copy.secure_cookies = @secure_cookies
         | 
| 120 | 
            +
                  copy.csp = self.class.send(:deep_copy_if_hash, @csp)
         | 
| 121 | 
            +
                  copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
         | 
| 122 | 
            +
                  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
         | 
| 123 | 
            +
                  copy
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def opt_out(header)
         | 
| 127 | 
            +
                  send("#{header}=", OPT_OUT)
         | 
| 128 | 
            +
                  if header == CSP::CONFIG_KEY
         | 
| 129 | 
            +
                    dynamic_csp = OPT_OUT
         | 
| 117 130 | 
             
                  end
         | 
| 131 | 
            +
                  self.cached_headers.delete(header)
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def update_x_frame_options(value)
         | 
| 135 | 
            +
                  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
         | 
| 136 | 
            +
                end
         | 
| 118 137 |  | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
                   | 
| 122 | 
            -
             | 
| 138 | 
            +
                # Public: generated cached headers for a specific user agent.
         | 
| 139 | 
            +
                def rebuild_csp_header_cache!(user_agent)
         | 
| 140 | 
            +
                  self.cached_headers[CSP::CONFIG_KEY] = {}
         | 
| 141 | 
            +
                  unless current_csp == OPT_OUT
         | 
| 142 | 
            +
                    user_agent = UserAgent.parse(user_agent)
         | 
| 143 | 
            +
                    variation = CSP.ua_to_variation(user_agent)
         | 
| 144 | 
            +
                    self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
         | 
| 123 145 | 
             
                  end
         | 
| 124 | 
            -
                  copy
         | 
| 125 146 | 
             
                end
         | 
| 126 147 |  | 
| 127 | 
            -
                 | 
| 128 | 
            -
             | 
| 129 | 
            -
                # Returns the value if available, and returns a dup of any hash values.
         | 
| 130 | 
            -
                def fetch(key)
         | 
| 131 | 
            -
                  config = send(key)
         | 
| 132 | 
            -
                  config = self.class.deep_copy(config) if config.is_a?(Hash)
         | 
| 133 | 
            -
                  config
         | 
| 148 | 
            +
                def current_csp
         | 
| 149 | 
            +
                  @dynamic_csp || @csp
         | 
| 134 150 | 
             
                end
         | 
| 135 151 |  | 
| 136 152 | 
             
                # Public: validates all configurations values.
         | 
| @@ -139,16 +155,32 @@ module SecureHeaders | |
| 139 155 | 
             
                #
         | 
| 140 156 | 
             
                # Returns nothing
         | 
| 141 157 | 
             
                def validate_config!
         | 
| 142 | 
            -
                  StrictTransportSecurity.validate_config!(hsts)
         | 
| 143 | 
            -
                  ContentSecurityPolicy.validate_config!(csp)
         | 
| 144 | 
            -
                  XFrameOptions.validate_config!(x_frame_options)
         | 
| 145 | 
            -
                  XContentTypeOptions.validate_config!(x_content_type_options)
         | 
| 146 | 
            -
                  XXssProtection.validate_config!(x_xss_protection)
         | 
| 147 | 
            -
                  XDownloadOptions.validate_config!(x_download_options)
         | 
| 148 | 
            -
                  XPermittedCrossDomainPolicies.validate_config!(x_permitted_cross_domain_policies)
         | 
| 149 | 
            -
                  PublicKeyPins.validate_config!(hpkp)
         | 
| 158 | 
            +
                  StrictTransportSecurity.validate_config!(@hsts)
         | 
| 159 | 
            +
                  ContentSecurityPolicy.validate_config!(@csp)
         | 
| 160 | 
            +
                  XFrameOptions.validate_config!(@x_frame_options)
         | 
| 161 | 
            +
                  XContentTypeOptions.validate_config!(@x_content_type_options)
         | 
| 162 | 
            +
                  XXssProtection.validate_config!(@x_xss_protection)
         | 
| 163 | 
            +
                  XDownloadOptions.validate_config!(@x_download_options)
         | 
| 164 | 
            +
                  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
         | 
| 165 | 
            +
                  PublicKeyPins.validate_config!(@hpkp)
         | 
| 150 166 | 
             
                end
         | 
| 151 167 |  | 
| 168 | 
            +
                protected
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                def csp=(new_csp)
         | 
| 171 | 
            +
                  if self.dynamic_csp
         | 
| 172 | 
            +
                    raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  @csp = new_csp
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                def cached_headers=(headers)
         | 
| 179 | 
            +
                  @cached_headers = headers
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                private
         | 
| 183 | 
            +
             | 
| 152 184 | 
             
                # Public: Precompute the header names and values for this configuraiton.
         | 
| 153 185 | 
             
                # Ensures that headers generated at configure time, not on demand.
         | 
| 154 186 | 
             
                #
         | 
| @@ -156,7 +188,7 @@ module SecureHeaders | |
| 156 188 | 
             
                def cache_headers!
         | 
| 157 189 | 
             
                  # generate defaults for the "easy" headers
         | 
| 158 190 | 
             
                  headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
         | 
| 159 | 
            -
                    config =  | 
| 191 | 
            +
                    config = instance_variable_get("@#{klass::CONFIG_KEY}")
         | 
| 160 192 | 
             
                    unless config == OPT_OUT
         | 
| 161 193 | 
             
                      hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
         | 
| 162 194 | 
             
                    end
         | 
| @@ -165,7 +197,7 @@ module SecureHeaders | |
| 165 197 | 
             
                  generate_csp_headers(headers)
         | 
| 166 198 |  | 
| 167 199 | 
             
                  headers.freeze
         | 
| 168 | 
            -
                   | 
| 200 | 
            +
                  self.cached_headers = headers
         | 
| 169 201 | 
             
                end
         | 
| 170 202 |  | 
| 171 203 | 
             
                # Private: adds CSP headers for each variation of CSP support.
         | 
| @@ -175,11 +207,10 @@ module SecureHeaders | |
| 175 207 | 
             
                #
         | 
| 176 208 | 
             
                # Returns nothing
         | 
| 177 209 | 
             
                def generate_csp_headers(headers)
         | 
| 178 | 
            -
                  unless csp == OPT_OUT
         | 
| 210 | 
            +
                  unless @csp == OPT_OUT
         | 
| 179 211 | 
             
                    headers[CSP::CONFIG_KEY] = {}
         | 
| 180 | 
            -
             | 
| 212 | 
            +
                    csp_config = self.current_csp
         | 
| 181 213 | 
             
                    CSP::VARIATIONS.each do |name, _|
         | 
| 182 | 
            -
                      csp_config = fetch(CSP::CONFIG_KEY)
         | 
| 183 214 | 
             
                      csp = CSP.make_header(csp_config, UserAgent.parse(name))
         | 
| 184 215 | 
             
                      headers[CSP::CONFIG_KEY][name] = csp.freeze
         | 
| 185 216 | 
             
                    end
         |