secure_headers 3.0.3 → 3.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8ce791545252f765e19db9b42b88d0dec3c7f14a
4
- data.tar.gz: 5a677c30789119f527ea04f940de663a98344339
3
+ metadata.gz: 3ebe74bc751469bb4305dd36b59fe4881a87ec3d
4
+ data.tar.gz: 83fed6de7e4cab010cab6010caa0ba005ac43fc7
5
5
  SHA512:
6
- metadata.gz: c861e7c69de1e69d4a53090a33b9c2f699900ee728d55694f0e7df7c32f430bebc65a9304cf0a8536c4663ed97c678340065a17a1ce6862c000b1c7984989d29
7
- data.tar.gz: cc33e4d244bbd8bdc46960594d5d6de0bae38bcac6b282f5be9a8a32807d6ad2eb737e28553335cc74223a8ede8c6d592207bd5ee168fb05bf91c04118c8ec69
6
+ metadata.gz: bd02880ba737a5a9489dd6e8209420259fec8d78330f5992e7b34f61ebe16677a93ff123dd3b417f6973bfcd85e03524b053bcd1487458492aa4c7b8a7a9bb40
7
+ data.tar.gz: 550c48cfd47e656e70dd3ab39ed6386b8d148231d55a553395da6076bc88bdaaa7fe1803695b093cf3e4fd49cb30bed0aff15a2455652efd00d7a27d9ce55636
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
- - "2.2"
5
- - "2.1"
6
- - "2.0.0"
7
- - "1.9.3"
8
- - "jruby-19mode"
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,29 @@
1
+ ## 3.1.2 Bug fix for regression
2
+
3
+ See https://github.com/twitter/secureheaders/pull/239
4
+
5
+ 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.
6
+
7
+ ## 3.1.1 Bug fix for regression
8
+
9
+ See https://github.com/twitter/secureheaders/pull/235
10
+
11
+ `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.
12
+
13
+ This is a regression in 3.1.0 only.
14
+
15
+ Now it returns true. I've added a test case to ensure that `header_hash_for` will never return such an element.
16
+
17
+ ## 3.1.0 Adding secure cookie support
18
+
19
+ 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.
20
+
21
+ 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:
22
+
23
+ 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.
24
+ 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.
25
+ 1. CSP header string is produced at most once per request.
26
+
1
27
  ## 3.0.3
2
28
 
3
29
  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 +40,7 @@ When appending:
14
40
  {
15
41
  report_uri => %w(https://report-uri.io/asdf)
16
42
  }
43
+ ```
17
44
 
18
45
  Would result in `default-src *; report-uri *` which doesn't make any sense at all.
19
46
 
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
@@ -15,6 +15,8 @@ The gem will automatically apply several headers that are related to security.
15
15
  - 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)
16
16
  - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
17
17
 
18
+ It can also mark all http cookies with the secure attribute (when configured to do so).
19
+
18
20
  `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
19
21
 
20
22
  ## Use
@@ -29,7 +31,8 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
29
31
 
30
32
  ```ruby
31
33
  SecureHeaders::Configuration.default do |config|
32
- config.hsts = "max-age=#{20.years.to_i}"
34
+ config.secure_cookies = true # mark all cookies as "secure"
35
+ config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
33
36
  config.x_frame_options = "DENY"
34
37
  config.x_content_type_options = "nosniff"
35
38
  config.x_xss_protection = "1; mode=block"
@@ -57,13 +60,13 @@ SecureHeaders::Configuration.default do |config|
57
60
  plugin_types: %w(application/x-shockwave-flash),
58
61
  block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
59
62
  upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
60
- report_uri: %w(https://example.com/uri-directive)
63
+ report_uri: %w(https://report-uri.io/example-csp)
61
64
  }
62
65
  config.hpkp = {
63
66
  report_only: false,
64
67
  max_age: 60.days.to_i,
65
68
  include_subdomains: true,
66
- report_uri: "https://example.com/uri-directive",
69
+ report_uri: "https://report-uri.io/example-hpkp",
67
70
  pins: [
68
71
  {sha256: "abc"},
69
72
  {sha256: "123"}
@@ -175,7 +178,7 @@ When manipulating content security policy, there are a few things to consider. T
175
178
 
176
179
  #### Append to the policy with a directive other than `default_src`
177
180
 
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:
181
+ 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
182
 
180
183
  ```ruby
181
184
  ::SecureHeaders::Configuration.default do |config|
@@ -255,7 +258,7 @@ config.hpkp = {
255
258
  {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
256
259
  ],
257
260
  report_only: true, # defaults to false (report-only mode)
258
- report_uri: '//example.com/uri-directive',
261
+ report_uri: 'https://report-uri.io/example-hpkp',
259
262
  app_name: 'example',
260
263
  tag_report_uri: true
261
264
  }
@@ -287,43 +290,6 @@ class Donkey < Sinatra::Application
287
290
  end
288
291
  ```
289
292
 
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
293
  ## Similar libraries
328
294
 
329
295
  * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers)
@@ -334,6 +300,8 @@ end
334
300
  * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security)
335
301
  * Go - [secureheader](https://github.com/kr/secureheader)
336
302
  * Elixir [secure_headers](https://github.com/anotherhale/secure_headers)
303
+ * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security)
304
+ * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/)
337
305
 
338
306
  ## License
339
307
 
@@ -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
- yield(override)
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.
@@ -82,20 +71,44 @@ module SecureHeaders
82
71
  ALL_HEADER_CLASSES.each do |klass|
83
72
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
84
73
  end
74
+ config.dynamic_csp = OPT_OUT
85
75
  end
86
76
 
87
77
  add_configuration(NOOP_CONFIGURATION, noop_config)
88
78
  end
79
+
80
+ # Public: perform a basic deep dup. The shallow copy provided by dup/clone
81
+ # can lead to modifying parent objects.
82
+ def deep_copy(config)
83
+ config.each_with_object({}) do |(key, value), hash|
84
+ hash[key] = if value.is_a?(Array)
85
+ value.dup
86
+ else
87
+ value
88
+ end
89
+ end
90
+ end
91
+
92
+ # Private: convenience method purely DRY things up. The value may not be a
93
+ # hash (e.g. OPT_OUT, nil)
94
+ def deep_copy_if_hash(value)
95
+ if value.is_a?(Hash)
96
+ deep_copy(value)
97
+ else
98
+ value
99
+ end
100
+ end
89
101
  end
90
102
 
91
- attr_accessor :hsts, :x_frame_options, :x_content_type_options,
92
- :x_xss_protection, :csp, :x_download_options, :x_permitted_cross_domain_policies,
93
- :hpkp
94
- attr_reader :cached_headers
103
+ attr_writer :hsts, :x_frame_options, :x_content_type_options,
104
+ :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
105
+ :hpkp, :dynamic_csp, :secure_cookies
106
+
107
+ attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies
95
108
 
96
109
  def initialize(&block)
97
110
  self.hpkp = OPT_OUT
98
- self.csp = self.class.deep_copy(CSP::DEFAULT_CONFIG)
111
+ self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
99
112
  instance_eval &block if block_given?
100
113
  end
101
114
 
@@ -104,33 +117,45 @@ module SecureHeaders
104
117
  # Returns a deep-dup'd copy of this configuration.
105
118
  def dup
106
119
  copy = self.class.new
107
- copy.hsts = hsts
108
- copy.x_frame_options = x_frame_options
109
- copy.x_content_type_options = x_content_type_options
110
- copy.x_xss_protection = x_xss_protection
111
- copy.x_download_options = x_download_options
112
- copy.x_permitted_cross_domain_policies = x_permitted_cross_domain_policies
113
- copy.csp = if csp.is_a?(Hash)
114
- self.class.deep_copy(csp)
115
- else
116
- csp
120
+ copy.secure_cookies = @secure_cookies
121
+ copy.csp = self.class.send(:deep_copy_if_hash, @csp)
122
+ copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
123
+ copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
124
+ copy.x_content_type_options = @x_content_type_options
125
+ copy.hsts = @hsts
126
+ copy.x_frame_options = @x_frame_options
127
+ copy.x_xss_protection = @x_xss_protection
128
+ copy.x_download_options = @x_download_options
129
+ copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
130
+ copy.hpkp = @hpkp
131
+ copy
132
+ end
133
+
134
+ def opt_out(header)
135
+ send("#{header}=", OPT_OUT)
136
+ if header == CSP::CONFIG_KEY
137
+ dynamic_csp = OPT_OUT
117
138
  end
139
+ self.cached_headers.delete(header)
140
+ end
141
+
142
+ def update_x_frame_options(value)
143
+ @x_frame_options = value
144
+ self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
145
+ end
118
146
 
119
- copy.hpkp = if hpkp.is_a?(Hash)
120
- self.class.deep_copy(hpkp)
121
- else
122
- hpkp
147
+ # Public: generated cached headers for a specific user agent.
148
+ def rebuild_csp_header_cache!(user_agent)
149
+ self.cached_headers[CSP::CONFIG_KEY] = {}
150
+ unless current_csp == OPT_OUT
151
+ user_agent = UserAgent.parse(user_agent)
152
+ variation = CSP.ua_to_variation(user_agent)
153
+ self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
123
154
  end
124
- copy
125
155
  end
126
156
 
127
- # Public: Retrieve a config based on the CONFIG_KEY for a class
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
157
+ def current_csp
158
+ @dynamic_csp || @csp
134
159
  end
135
160
 
136
161
  # Public: validates all configurations values.
@@ -139,16 +164,32 @@ module SecureHeaders
139
164
  #
140
165
  # Returns nothing
141
166
  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)
167
+ StrictTransportSecurity.validate_config!(@hsts)
168
+ ContentSecurityPolicy.validate_config!(@csp)
169
+ XFrameOptions.validate_config!(@x_frame_options)
170
+ XContentTypeOptions.validate_config!(@x_content_type_options)
171
+ XXssProtection.validate_config!(@x_xss_protection)
172
+ XDownloadOptions.validate_config!(@x_download_options)
173
+ XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
174
+ PublicKeyPins.validate_config!(@hpkp)
150
175
  end
151
176
 
177
+ protected
178
+
179
+ def csp=(new_csp)
180
+ if self.dynamic_csp
181
+ raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
182
+ end
183
+
184
+ @csp = new_csp
185
+ end
186
+
187
+ def cached_headers=(headers)
188
+ @cached_headers = headers
189
+ end
190
+
191
+ private
192
+
152
193
  # Public: Precompute the header names and values for this configuraiton.
153
194
  # Ensures that headers generated at configure time, not on demand.
154
195
  #
@@ -156,7 +197,7 @@ module SecureHeaders
156
197
  def cache_headers!
157
198
  # generate defaults for the "easy" headers
158
199
  headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
159
- config = fetch(klass::CONFIG_KEY)
200
+ config = instance_variable_get("@#{klass::CONFIG_KEY}")
160
201
  unless config == OPT_OUT
161
202
  hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
162
203
  end
@@ -165,7 +206,7 @@ module SecureHeaders
165
206
  generate_csp_headers(headers)
166
207
 
167
208
  headers.freeze
168
- @cached_headers = headers
209
+ self.cached_headers = headers
169
210
  end
170
211
 
171
212
  # Private: adds CSP headers for each variation of CSP support.
@@ -175,11 +216,10 @@ module SecureHeaders
175
216
  #
176
217
  # Returns nothing
177
218
  def generate_csp_headers(headers)
178
- unless csp == OPT_OUT
219
+ unless @csp == OPT_OUT
179
220
  headers[CSP::CONFIG_KEY] = {}
180
-
221
+ csp_config = self.current_csp
181
222
  CSP::VARIATIONS.each do |name, _|
182
- csp_config = fetch(CSP::CONFIG_KEY)
183
223
  csp = CSP.make_header(csp_config, UserAgent.parse(name))
184
224
  headers[CSP::CONFIG_KEY][name] = csp.freeze
185
225
  end