secure_headers 3.3.1 → 3.4.1

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: 81af1b59ac5a5a838557fb48a3b605c5202bcd8f
4
- data.tar.gz: 5b5ecdf10eea6b5a8e224e6b1606c554105775c6
3
+ metadata.gz: 45e392304147a02bee8cf0709fe52e16f9ad255c
4
+ data.tar.gz: e07030aa93c84a20e9b22d88af6489d8a71c95e7
5
5
  SHA512:
6
- metadata.gz: e3c87eb56ae112ae7048592fe082a70cf215603133b5f1385f57b5dbe080ab74c61eb4b093d3dc911a4f837e8d57a16a88a420a49195311ecd375d838ad4edd2
7
- data.tar.gz: 2708e94fffbaa40e649c30925142fd16d8e79236f4cefdbcb64b58a4e08941100961f891d23754798d08c681309341717fd49597df281d601e3e3edf1b09a524
6
+ metadata.gz: 8238f728eb74303b6aac54d40e3eaf1aacdaf7e0c910fe9a05f3dc005daa9eb96876391619eb2cb613a0d2a8ad358a48bc899626be46d201561f05064f47fdf8
7
+ data.tar.gz: d0d448274a7a4789f668ac59c49903bf50889a2675a82d62d91ca721c5160b50288b5fafcf8b041422f575cc42d31b4a614fbc8036759fb522c149131a472f33
@@ -0,0 +1,41 @@
1
+ # Feature Requests
2
+
3
+ ## Adding a new header
4
+
5
+ Generally, adding a new header is always OK.
6
+
7
+ * Is the header supported by any user agent? If so, which?
8
+ * What does it do?
9
+ * What are the valid values for the header?
10
+ * Where does the specification live?
11
+
12
+ ## Adding a new CSP directive
13
+
14
+ * Is the directive supported by any user agent? If so, which?
15
+ * What does it do?
16
+ * What are the valid values for the directive?
17
+
18
+ ---
19
+
20
+ # Bugs
21
+
22
+ 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.
23
+
24
+ ### Expected outcome
25
+
26
+ Describe what you expected to happen
27
+
28
+ 1. I configure CSP to do X
29
+ 1. When I inspect the response headers, the CSP should have included X
30
+
31
+ ### Actual outcome
32
+
33
+ 1. The generated policy did not include X
34
+
35
+ ### Config
36
+
37
+ Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`).
38
+
39
+ ### Generated headers
40
+
41
+ Provide a sample response containing the headers
@@ -0,0 +1,20 @@
1
+ ## All PRs:
2
+
3
+ * [ ] Has tests
4
+ * [ ] Documentation updated
5
+
6
+ ## Adding a new header
7
+
8
+ Generally, adding a new header is always OK.
9
+
10
+ * Is the header supported by any user agent? If so, which?
11
+ * What does it do?
12
+ * What are the valid values for the header?
13
+ * Where does the specification live?
14
+
15
+ ## Adding a new CSP directive
16
+
17
+ * Is the directive supported by any user agent? If so, which?
18
+ * What does it do?
19
+ * What are the valid values for the directive?
20
+
data/CHANGELOG.md CHANGED
@@ -1,3 +1,82 @@
1
+ ## 3.4.1 Named Appends
2
+
3
+ ### Small bugfix
4
+
5
+ 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).
6
+
7
+ ### Named Appends
8
+
9
+ 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.
10
+
11
+ ```ruby
12
+ def show
13
+ if include_widget?
14
+ @widget = widget.render
15
+ use_content_security_policy_named_append(:widget_partial)
16
+ end
17
+ end
18
+
19
+
20
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
21
+ if request.controller_instance.current_user.in_test_bucket?
22
+ SecureHeaders.override_x_frame_options(request, "DENY")
23
+ { child_src: %w(beta.thirdpartyhost.com) }
24
+ else
25
+ { child_src: %w(thirdpartyhost.com) }
26
+ end
27
+ end
28
+ ```
29
+
30
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
31
+
32
+ ```ruby
33
+ SecureHeader::Configuration.default do |config|
34
+ config.csp = { default_src: %w('self')}
35
+ end
36
+
37
+ SecureHeaders::Configuration.named_append(:A) do |request|
38
+ { default_src: %w(myhost.com) }
39
+ end
40
+
41
+ SecureHeaders::Configuration.named_append(:B) do |request|
42
+ { script_src: %w('unsafe-eval') }
43
+ end
44
+ ```
45
+
46
+ 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.):
47
+
48
+ ```ruby
49
+ def index
50
+ use_content_security_policy_named_append(:A)
51
+ use_content_security_policy_named_append(:B)
52
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
53
+ end
54
+
55
+ def show
56
+ use_content_security_policy_named_append(:B)
57
+ use_content_security_policy_named_append(:A)
58
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
59
+ end
60
+ ```
61
+
62
+ ## 3.4.0 the frame-src/child-src transition for Firefox.
63
+
64
+ Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
65
+
66
+ ```ruby
67
+ if supported_directives.include?(:child_src)
68
+ @config[:child_src] = @config[:child_src] || @config[:frame_src]
69
+ else
70
+ @config[:frame_src] = @config[:frame_src] || @config[:child_src]
71
+ end
72
+ ```
73
+
74
+ 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.
75
+
76
+ ## 3.3.2 minor fix to silence warnings when using rake
77
+
78
+ [@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants.
79
+
1
80
  ## 3.3.1 bugfix for boolean CSP directives
2
81
 
3
82
  [@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.
data/Gemfile CHANGED
@@ -5,7 +5,8 @@ gemspec
5
5
  group :test do
6
6
  gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0
7
7
  gem "pry-nav"
8
- gem "rack"
8
+ gem "json", "~> 1"
9
+ gem "rack", "~> 1"
9
10
  gem "rspec"
10
11
  gem "coveralls"
11
12
  end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders)
1
+ # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)
2
2
 
3
3
 
4
4
  **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
@@ -8,7 +8,7 @@
8
8
  The gem will automatically apply several headers that are related to security. This includes:
9
9
  - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/)
10
10
  - 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)
11
- - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options draft](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02)
11
+ - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034)
12
12
  - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
13
13
  - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx)
14
14
  - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx)
@@ -53,20 +53,19 @@ SecureHeaders::Configuration.default do |config|
53
53
 
54
54
  # directive values: these values will directly translate into source directives
55
55
  default_src: %w(https: 'self'),
56
- frame_src: %w('self' *.twimg.com itunes.apple.com),
57
- connect_src: %w(wws:),
56
+ base_uri: %w('self'),
57
+ block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
58
+ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
59
+ connect_src: %w(wss:),
58
60
  font_src: %w('self' data:),
61
+ form_action: %w('self' github.com),
62
+ frame_ancestors: %w('none'),
59
63
  img_src: %w(mycdn.com data:),
60
64
  media_src: %w(utoob.com),
61
65
  object_src: %w('self'),
66
+ plugin_types: %w(application/x-shockwave-flash),
62
67
  script_src: %w('self'),
63
68
  style_src: %w('unsafe-inline'),
64
- base_uri: %w('self'),
65
- child_src: %w('self'),
66
- form_action: %w('self' github.com),
67
- frame_ancestors: %w('none'),
68
- plugin_types: %w(application/x-shockwave-flash),
69
- block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
70
69
  upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
71
70
  report_uri: %w(https://report-uri.io/example-csp)
72
71
  }
@@ -95,6 +94,62 @@ use SecureHeaders::Middleware
95
94
 
96
95
  All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers).
97
96
 
97
+ ## Named Appends
98
+
99
+ 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.
100
+
101
+ ```ruby
102
+ def show
103
+ if include_widget?
104
+ @widget = widget.render
105
+ use_content_security_policy_named_append(:widget_partial)
106
+ end
107
+ end
108
+
109
+
110
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
111
+ SecureHeaders.override_x_frame_options(request, "DENY")
112
+ if request.controller_instance.current_user.in_test_bucket?
113
+ { child_src: %w(beta.thirdpartyhost.com) }
114
+ else
115
+ { child_src: %w(thirdpartyhost.com) }
116
+ end
117
+ end
118
+ ```
119
+
120
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
121
+
122
+ ```ruby
123
+ SecureHeader::Configuration.default do |config|
124
+ config.csp = { default_src: %w('self')}
125
+ end
126
+
127
+ SecureHeaders::Configuration.named_append(:A) do |request|
128
+ { default_src: %w(myhost.com) }
129
+ end
130
+
131
+ SecureHeaders::Configuration.named_append(:B) do |request|
132
+ { script_src: %w('unsafe-eval') }
133
+ end
134
+ ```
135
+
136
+ 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.):
137
+
138
+ ```ruby
139
+ def index
140
+ use_content_security_policy_named_append(:A)
141
+ use_content_security_policy_named_append(:B)
142
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
143
+ end
144
+
145
+ def show
146
+ use_content_security_policy_named_append(:B)
147
+ use_content_security_policy_named_append(:A)
148
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
149
+ end
150
+ ```
151
+
152
+
98
153
  ## Named overrides
99
154
 
100
155
  Named overrides serve two purposes:
@@ -126,7 +181,7 @@ end
126
181
 
127
182
  class MyController < ApplicationController
128
183
  def index
129
- # Produces default-src 'self'; script-src example.org otherdomain.org
184
+ # Produces default-src 'self'; script-src example.org otherdomain.com
130
185
  use_secure_headers_override(:script_from_otherdomain_com)
131
186
  end
132
187
 
@@ -46,6 +46,17 @@ module SecureHeaders
46
46
  @configurations[name]
47
47
  end
48
48
 
49
+ def named_appends(name)
50
+ @appends ||= {}
51
+ @appends[name]
52
+ end
53
+
54
+ def named_append(name, target = nil, &block)
55
+ @appends ||= {}
56
+ raise "Provide a configuration block" unless block_given?
57
+ @appends[name] = block
58
+ end
59
+
49
60
  private
50
61
 
51
62
  # Private: add a valid configuration to the global set of named configs.
@@ -203,7 +214,7 @@ module SecureHeaders
203
214
  raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
204
215
  end
205
216
 
206
- @csp = new_csp
217
+ @csp = self.class.send(:deep_copy_if_hash, new_csp)
207
218
  end
208
219
 
209
220
  def cookies=(cookies)
@@ -215,7 +226,7 @@ module SecureHeaders
215
226
  end
216
227
 
217
228
  def hpkp=(hpkp)
218
- @hpkp = hpkp
229
+ @hpkp = self.class.send(:deep_copy_if_hash, hpkp)
219
230
  end
220
231
 
221
232
  def hpkp_report_host=(hpkp_report_host)
@@ -1,18 +1,22 @@
1
1
  require_relative 'policy_management'
2
+ require 'useragent'
2
3
 
3
4
  module SecureHeaders
4
5
  class ContentSecurityPolicyConfigError < StandardError; end
5
6
  class ContentSecurityPolicy
6
7
  include PolicyManagement
7
8
 
9
+ # constants to be used for version-specific UA sniffing
10
+ VERSION_46 = ::UserAgent::Version.new("46")
11
+
8
12
  def initialize(config = nil, user_agent = OTHER)
9
- config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
10
- @config = config
13
+ @config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG)
11
14
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
12
15
  user_agent
13
16
  else
14
17
  UserAgent.parse(user_agent)
15
18
  end
19
+ normalize_child_frame_src
16
20
  @report_only = @config[:report_only]
17
21
  @preserve_schemes = @config[:preserve_schemes]
18
22
  @script_nonce = @config[:script_nonce]
@@ -42,6 +46,22 @@ module SecureHeaders
42
46
 
43
47
  private
44
48
 
49
+ # frame-src is deprecated, child-src is being implemented. They are
50
+ # very similar and in most cases, the same value can be used for both.
51
+ def normalize_child_frame_src
52
+ if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src]
53
+ Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
54
+ elsif @config[:frame_src]
55
+ Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
56
+ end
57
+
58
+ if supported_directives.include?(:child_src)
59
+ @config[:child_src] = @config[:child_src] || @config[:frame_src]
60
+ else
61
+ @config[:frame_src] = @config[:frame_src] || @config[:child_src]
62
+ end
63
+ end
64
+
45
65
  # Private: converts the config object into a string representing a policy.
46
66
  # Places default-src at the first directive and report-uri as the last. All
47
67
  # others are presented in alphabetical order.
@@ -162,9 +182,19 @@ module SecureHeaders
162
182
 
163
183
  # Private: determine which directives are supported for the given user agent.
164
184
  #
185
+ # Add UA-sniffing special casing here.
186
+ #
165
187
  # Returns an array of symbols representing the directives.
166
188
  def supported_directives
167
- @supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
189
+ @supported_directives ||= if VARIATIONS[@parsed_ua.browser]
190
+ if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
191
+ VARIATIONS["FirefoxTransitional"]
192
+ else
193
+ VARIATIONS[@parsed_ua.browser]
194
+ end
195
+ else
196
+ VARIATIONS[OTHER]
197
+ end
168
198
  end
169
199
 
170
200
  def nonces_supported?
@@ -53,16 +53,6 @@ module SecureHeaders
53
53
  FRAME_ANCESTORS = :frame_ancestors
54
54
  PLUGIN_TYPES = :plugin_types
55
55
 
56
- # These are directives that do not inherit the default-src value. This is
57
- # useful when calling #combine_policies.
58
- NON_FETCH_SOURCES = [
59
- BASE_URI,
60
- FORM_ACTION,
61
- FRAME_ANCESTORS,
62
- PLUGIN_TYPES,
63
- REPORT_URI
64
- ]
65
-
66
56
  DIRECTIVES_2_0 = [
67
57
  DIRECTIVES_1_0,
68
58
  BASE_URI,
@@ -100,10 +90,23 @@ module SecureHeaders
100
90
  PLUGIN_TYPES
101
91
  ].freeze
102
92
 
93
+ FIREFOX_46_DEPRECATED_DIRECTIVES = [
94
+ FRAME_SRC
95
+ ].freeze
96
+
97
+ FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
98
+ BLOCK_ALL_MIXED_CONTENT,
99
+ PLUGIN_TYPES
100
+ ].freeze
101
+
103
102
  FIREFOX_DIRECTIVES = (
104
103
  DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
105
104
  ).freeze
106
105
 
106
+ FIREFOX_46_DIRECTIVES = (
107
+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
108
+ ).freeze
109
+
107
110
  CHROME_DIRECTIVES = (
108
111
  DIRECTIVES_2_0 + DIRECTIVES_DRAFT
109
112
  ).freeze
@@ -114,10 +117,23 @@ module SecureHeaders
114
117
  # everything else is in between.
115
118
  BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
116
119
 
120
+ # These are directives that do not inherit the default-src value. This is
121
+ # useful when calling #combine_policies.
122
+ NON_FETCH_SOURCES = [
123
+ BASE_URI,
124
+ FORM_ACTION,
125
+ FRAME_ANCESTORS,
126
+ PLUGIN_TYPES,
127
+ REPORT_URI
128
+ ]
129
+
130
+ FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
131
+
117
132
  VARIATIONS = {
118
133
  "Chrome" => CHROME_DIRECTIVES,
119
134
  "Opera" => CHROME_DIRECTIVES,
120
135
  "Firefox" => FIREFOX_DIRECTIVES,
136
+ "FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
121
137
  "Safari" => SAFARI_DIRECTIVES,
122
138
  "Edge" => EDGE_DIRECTIVES,
123
139
  "Other" => CHROME_DIRECTIVES
@@ -254,8 +270,23 @@ module SecureHeaders
254
270
  def populate_fetch_source_with_default!(original, additions)
255
271
  # in case we would be appending to an empty directive, fill it with the default-src value
256
272
  additions.keys.each do |directive|
257
- unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
258
- original[directive] = original[:default_src]
273
+ if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
274
+ if nonce_added?(original, additions)
275
+ inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
276
+ unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
277
+ original[inferred_directive] = original[:default_src]
278
+ end
279
+ else
280
+ original[directive] = original[:default_src]
281
+ end
282
+ end
283
+ end
284
+ end
285
+
286
+ def nonce_added?(original, additions)
287
+ [:script_nonce, :style_nonce].each do |nonce|
288
+ if additions[nonce] && !original[nonce]
289
+ return true
259
290
  end
260
291
  end
261
292
  end
@@ -108,8 +108,6 @@ module SecureHeaders
108
108
  end
109
109
  end
110
110
 
111
- module ActionView #:nodoc:
112
- class Base #:nodoc:
113
- include SecureHeaders::ViewHelpers
114
- end
115
- end
111
+ ActiveSupport.on_load :action_view do
112
+ include SecureHeaders::ViewHelpers
113
+ end if defined?(ActiveSupport)
@@ -72,6 +72,11 @@ module SecureHeaders
72
72
  override_secure_headers_request_config(request, config)
73
73
  end
74
74
 
75
+ def use_content_security_policy_named_append(request, name)
76
+ additions = SecureHeaders::Configuration.named_appends(name).call(request)
77
+ append_content_security_policy_directives(request, additions)
78
+ end
79
+
75
80
  # Public: override X-Frame-Options settings for this request.
76
81
  #
77
82
  # value - deny, sameorigin, or allowall
@@ -267,4 +272,8 @@ module SecureHeaders
267
272
  def override_x_frame_options(value)
268
273
  SecureHeaders.override_x_frame_options(request, value)
269
274
  end
275
+
276
+ def use_content_security_policy_named_append(name)
277
+ SecureHeaders.use_content_security_policy_named_append(request, name)
278
+ end
270
279
  end
data/lib/tasks/tasks.rake CHANGED
@@ -1,7 +1,7 @@
1
- INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
2
- INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
3
- INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
4
- INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
1
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
2
+ INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
3
+ INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
4
+ INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
5
5
 
6
6
  namespace :secure_headers do
7
7
  include SecureHeaders::HashHelper
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.3.1"
4
+ gem.version = "3.4.1"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Security related headers all in one gem.'
@@ -24,7 +24,7 @@ module SecureHeaders
24
24
 
25
25
  describe "#value" do
26
26
  it "discards 'none' values if any other source expressions are present" do
27
- csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
27
+ csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
28
28
  expect(csp.value).not_to include("'none'")
29
29
  end
30
30
 
@@ -86,42 +86,68 @@ module SecureHeaders
86
86
  expect(csp.value).to eq("default-src example.org")
87
87
  end
88
88
 
89
+ it "emits a warning when using frame-src" do
90
+ expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./)
91
+ ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
92
+ end
93
+
94
+ it "emits a warning when child-src and frame-src are supplied but are not equal" do
95
+ expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./)
96
+ ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
97
+ end
98
+
99
+ it "will still set inconsistent child/frame-src values to be less surprising" do
100
+ expect(Kernel).to receive(:warn).at_least(:once)
101
+ firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value
102
+ firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value
103
+ expect(firefox).not_to eq(firefox_transitional)
104
+ expect(firefox).to match(/frame-src/)
105
+ expect(firefox).not_to match(/child-src/)
106
+ expect(firefox_transitional).to match(/child-src/)
107
+ expect(firefox_transitional).not_to match(/frame-src/)
108
+ end
109
+
89
110
  context "browser sniffing" do
90
111
  let (:complex_opts) do
91
- ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
92
- hash[directive] = %w('self')
112
+ (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
113
+ hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
93
114
  end.merge({
94
115
  block_all_mixed_content: true,
95
116
  upgrade_insecure_requests: true,
96
117
  reflected_xss: "block",
97
- script_src: %w('self'),
118
+ script_src: %w(script-src.com),
98
119
  script_nonce: 123456
99
120
  })
100
121
  end
101
122
 
102
123
  it "does not filter any directives for Chrome" do
103
124
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
104
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
125
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
105
126
  end
106
127
 
107
128
  it "does not filter any directives for Opera" do
108
129
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
109
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
130
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
110
131
  end
111
132
 
112
133
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
113
134
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
114
- expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
135
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
136
+ end
137
+
138
+ it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
139
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
140
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
115
141
  end
116
142
 
117
- it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
143
+ it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
118
144
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
119
- expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
145
+ expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
120
146
  end
121
147
 
122
- it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
148
+ it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
123
149
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
124
- expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
150
+ expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
125
151
  end
126
152
  end
127
153
  end
@@ -23,7 +23,8 @@ module SecureHeaders
23
23
  # directive values: these values will directly translate into source directives
24
24
  default_src: %w(https: 'self'),
25
25
  frame_src: %w('self' *.twimg.com itunes.apple.com),
26
- connect_src: %w(wws:),
26
+ child_src: %w('self' *.twimg.com itunes.apple.com),
27
+ connect_src: %w(wss:),
27
28
  font_src: %w('self' data:),
28
29
  img_src: %w(mycdn.com data:),
29
30
  media_src: %w(utoob.com),
@@ -31,7 +32,6 @@ module SecureHeaders
31
32
  script_src: %w('self'),
32
33
  style_src: %w('unsafe-inline'),
33
34
  base_uri: %w('self'),
34
- child_src: %w('self'),
35
35
  form_action: %w('self' github.com),
36
36
  frame_ancestors: %w('none'),
37
37
  plugin_types: %w(application/x-shockwave-flash),
@@ -90,8 +90,7 @@ module SecureHeaders
90
90
  config = Configuration.default do |config|
91
91
  config.csp = {
92
92
  default_src: %w('self'),
93
- child_src: %w('self'), #unsupported by firefox
94
- frame_src: %w('self')
93
+ child_src: %w('self')
95
94
  }
96
95
  end
97
96
  firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
@@ -102,6 +101,8 @@ module SecureHeaders
102
101
  SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
103
102
 
104
103
  hash = SecureHeaders.header_hash_for(firefox_request)
104
+
105
+ # child-src is translated to frame-src
105
106
  expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
106
107
  end
107
108
 
@@ -137,6 +138,10 @@ module SecureHeaders
137
138
  end
138
139
 
139
140
  context "content security policy" do
141
+ let(:chrome_request) {
142
+ Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
143
+ }
144
+
140
145
  it "appends a value to csp directive" do
141
146
  Configuration.default do |config|
142
147
  config.csp = {
@@ -150,6 +155,52 @@ module SecureHeaders
150
155
  expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
151
156
  end
152
157
 
158
+ it "supports named appends" do
159
+ Configuration.default do |config|
160
+ config.csp = {
161
+ default_src: %w('self')
162
+ }
163
+ end
164
+
165
+ Configuration.named_append(:moar_default_sources) do |request|
166
+ { default_src: %w(https:)}
167
+ end
168
+
169
+ Configuration.named_append(:how_about_a_script_src_too) do |request|
170
+ { script_src: %w('unsafe-inline')}
171
+ end
172
+
173
+ SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
174
+ SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
175
+ hash = SecureHeaders.header_hash_for(request)
176
+
177
+ expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
178
+ end
179
+
180
+ it "appends a nonce to a missing script-src value" do
181
+ Configuration.default do |config|
182
+ config.csp = {
183
+ default_src: %w('self')
184
+ }
185
+ end
186
+
187
+ SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
188
+ hash = SecureHeaders.header_hash_for(chrome_request)
189
+ expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
190
+ end
191
+
192
+ it "appends a hash to a missing script-src value" do
193
+ Configuration.default do |config|
194
+ config.csp = {
195
+ default_src: %w('self')
196
+ }
197
+ end
198
+
199
+ SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
200
+ hash = SecureHeaders.header_hash_for(chrome_request)
201
+ expect(hash[CSP::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
202
+ end
203
+
153
204
  it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
154
205
  Configuration.default do |config|
155
206
  config.csp = {
@@ -233,7 +284,6 @@ module SecureHeaders
233
284
  }
234
285
  end
235
286
 
236
- chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
237
287
  nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
238
288
 
239
289
  # simulate the nonce being used multiple times in a request:
data/spec/spec_helper.rb CHANGED
@@ -12,7 +12,8 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
12
12
 
13
13
  USER_AGENTS = {
14
14
  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",
15
- firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
15
+ firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
16
+ firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
16
17
  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',
17
18
  ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
18
19
  opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
data/upgrading-to-3-0.md CHANGED
@@ -3,17 +3,19 @@
3
3
  Changes
4
4
  ==
5
5
 
6
- | What | < = 2.x | >= 3.0 |
7
- |----------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
8
- | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
9
- | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
10
- | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
11
- | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_script_nonce` or `content_security_policy_style_nonce`
12
- | `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
13
- | `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
14
- | 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) |
15
- | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
16
- | 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 |
6
+ | What | < = 2.x | >= 3.0 |
7
+ | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
9
+ | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
10
+ | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
11
+ | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` |
12
+ | 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). |
13
+ | `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
14
+ | `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
15
+ | 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) |
16
+ | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
17
+ | 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 |
18
+ | 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` |
17
19
 
18
20
  Migrating to 3.x from <= 2.x
19
21
  ==
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secure_headers
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-09 00:00:00.000000000 Z
11
+ date: 2016-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -45,6 +45,8 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/ISSUE_TEMPLATE.md"
49
+ - ".github/PULL_REQUEST_TEMPLATE.md"
48
50
  - ".gitignore"
49
51
  - ".rspec"
50
52
  - ".ruby-gemset"