secure_headers 3.6.4 → 3.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fec7efedf547e7aff5f7193f3306802367df524e
4
- data.tar.gz: 5d4a99a2f5989e2c119b6d8be2ed232850635855
3
+ metadata.gz: 591de0f41bc64b3520fbb2fdab9fc94b490d1604
4
+ data.tar.gz: f836c5e63e4d054ff2ce3f1ec597bbd9cf67ed4b
5
5
  SHA512:
6
- metadata.gz: d5ca176a980b3acf9d97442d13e6109c5a8e8043f796733b3ffbee16f45554acd1f46ee2874fd3099be4f9217618b1591e75ec1ac6c7bc7e2e49acce3e06dbcf
7
- data.tar.gz: 9881f2a604975c4d1f64b4cbc690c063751b8f573f0db87a5c2593326fa5ffcdb83e533d95d9a1ce46551d97192c7faa48e6a5f9485765252d25f53b66d168f1
6
+ metadata.gz: 5317d425d7c65dfc73b32703aae7e16e3a566b620b42e3233cd6dc7403a1a9842bb7dc00ffc5c13a86240c5a8abee3ae07fed0c73d4b06a06bcc11db39a79a20
7
+ data.tar.gz: 154573bfb9112b4b1510abbde3f5eb1e85b709f7839ff4a6a547223e67e698ff014bb578f02b0e8775ee723016f5903e8cda38cb9846b24cafe3dc8e414bb007
data/.rspec CHANGED
@@ -1 +1,2 @@
1
1
  --order rand
2
+ --warnings
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 3.7.0
2
+
3
+ Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322)
4
+
5
+ ## 3.6.7
6
+
7
+ Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva!
8
+
9
+ ## 3.6.6
10
+
11
+ wat?
12
+
13
+ ## 3.6.5
14
+
15
+ Update clear-site-data header to use current format specified by the specification.
16
+
1
17
  ## 3.6.4
2
18
 
3
19
  Fix case where mixing frame-src/child-src dynamically would behave in unexpected ways: https://github.com/twitter/secureheaders/pull/325
@@ -196,7 +212,7 @@ You can add hash sources directly to your policy :
196
212
  rake secure_headers:generate_hashes
197
213
  ```
198
214
 
199
- This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
215
+ This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
200
216
 
201
217
  ```yaml
202
218
  ---
data/CONTRIBUTING.md CHANGED
@@ -15,7 +15,7 @@ Please note that this project is released with a [Contributor Code of Conduct][c
15
15
  0. Configure and install the dependencies: `bundle install`
16
16
  0. Make sure the tests pass on your machine: `bundle exec rspec spec`
17
17
  0. Create a new branch: `git checkout -b my-branch-name`
18
- 0. Make your change, add tests, and make sure the tests still pass
18
+ 0. Make your change, add tests, and make sure the tests still pass and that no warnings are raised
19
19
  0. Push to your fork and [submit a pull request][pr]
20
20
  0. Pat your self on the back and wait for your pull request to be reviewed and merged.
21
21
 
@@ -25,7 +25,7 @@ Here are a few things you can do that will increase the likelihood of your pull
25
25
  - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
26
26
  - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
27
27
 
28
- ## Releasing
28
+ ## Releasing
29
29
 
30
30
  0. Ensure CI is green
31
31
  0. Pull the latest code
@@ -39,5 +39,3 @@ Here are a few things you can do that will increase the likelihood of your pull
39
39
 
40
40
  - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
41
41
  - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
42
-
43
-
data/README.md CHANGED
@@ -18,7 +18,8 @@ The gem will automatically apply several headers that are related to security.
18
18
  - 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)
19
19
  - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
20
20
  - 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)
21
- - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://www.w3.org/TR/clear-site-data/).
21
+ - Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/).
22
+ - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/).
22
23
 
23
24
  It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
24
25
 
@@ -76,6 +77,11 @@ SecureHeaders::Configuration.default do |config|
76
77
  "storage",
77
78
  "executionContexts"
78
79
  ]
80
+ config.expect_certificate_transparency = {
81
+ enforce: false,
82
+ max_age: 1.day.to_i,
83
+ report_uri: "https://report-uri.io/example-ct"
84
+ }
79
85
  config.csp = {
80
86
  # "meta" values. these will shaped the header, but the values are not included in the header.
81
87
  # report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
@@ -91,6 +97,7 @@ SecureHeaders::Configuration.default do |config|
91
97
  form_action: %w('self' github.com),
92
98
  frame_ancestors: %w('none'),
93
99
  img_src: %w(mycdn.com data:),
100
+ manifest_src: %w('self'),
94
101
  media_src: %w(utoob.com),
95
102
  object_src: %w('self'),
96
103
  plugin_types: %w(application/x-shockwave-flash),
data/docs/hashes.md CHANGED
@@ -21,7 +21,7 @@ You can add hash sources directly to your policy :
21
21
  rake secure_headers:generate_hashes
22
22
  ```
23
23
 
24
- This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
24
+ This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
25
25
 
26
26
  ```yaml
27
27
  ---
@@ -31,7 +31,7 @@ module SecureHeaders
31
31
  raise NotYetConfiguredError, "#{base} policy not yet supplied"
32
32
  end
33
33
  override = @configurations[base].dup
34
- override.instance_eval &block if block_given?
34
+ override.instance_eval(&block) if block_given?
35
35
  add_configuration(name, override)
36
36
  end
37
37
 
@@ -116,23 +116,41 @@ module SecureHeaders
116
116
 
117
117
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
118
118
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
119
- :referrer_policy, :clear_site_data
119
+ :referrer_policy, :clear_site_data, :expect_certificate_transparency
120
120
 
121
121
  attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
122
122
 
123
+ @script_hashes = nil
124
+ @style_hashes = nil
125
+
123
126
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
124
- if File.exists?(HASH_CONFIG_FILE)
127
+ if File.exist?(HASH_CONFIG_FILE)
125
128
  config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
126
129
  @script_hashes = config["scripts"]
127
130
  @style_hashes = config["styles"]
128
131
  end
129
132
 
130
133
  def initialize(&block)
134
+ @cookies = nil
135
+ @clear_site_data = nil
136
+ @csp = nil
137
+ @csp_report_only = nil
138
+ @hpkp_report_host = nil
139
+ @hpkp = nil
140
+ @hsts = nil
141
+ @x_content_type_options = nil
142
+ @x_download_options = nil
143
+ @x_frame_options = nil
144
+ @x_permitted_cross_domain_policies = nil
145
+ @x_xss_protection = nil
146
+ @expect_certificate_transparency = nil
147
+
131
148
  self.hpkp = OPT_OUT
132
149
  self.referrer_policy = OPT_OUT
133
150
  self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
134
151
  self.csp_report_only = OPT_OUT
135
- instance_eval &block if block_given?
152
+
153
+ instance_eval(&block) if block_given?
136
154
  end
137
155
 
138
156
  # Public: copy everything but the cached headers
@@ -151,6 +169,7 @@ module SecureHeaders
151
169
  copy.x_download_options = @x_download_options
152
170
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
153
171
  copy.clear_site_data = @clear_site_data
172
+ copy.expect_certificate_transparency = @expect_certificate_transparency
154
173
  copy.referrer_policy = @referrer_policy
155
174
  copy.hpkp = @hpkp
156
175
  copy.hpkp_report_host = @hpkp_report_host
@@ -183,6 +202,7 @@ module SecureHeaders
183
202
  XDownloadOptions.validate_config!(@x_download_options)
184
203
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
185
204
  ClearSiteData.validate_config!(@clear_site_data)
205
+ ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
186
206
  PublicKeyPins.validate_config!(@hpkp)
187
207
  Cookie.validate_config!(@cookies)
188
208
  end
@@ -2,7 +2,6 @@ module SecureHeaders
2
2
  class ClearSiteDataConfigError < StandardError; end
3
3
  class ClearSiteData
4
4
  HEADER_NAME = "Clear-Site-Data".freeze
5
- TYPES = "types".freeze
6
5
 
7
6
  # Valid `types`
8
7
  CACHE = "cache".freeze
@@ -22,9 +21,9 @@ module SecureHeaders
22
21
  when nil, OPT_OUT, []
23
22
  # noop
24
23
  when Array
25
- [HEADER_NAME, JSON.dump(TYPES => config)]
24
+ [HEADER_NAME, make_header_value(config)]
26
25
  when true
27
- [HEADER_NAME, JSON.dump(TYPES => ALL_TYPES)]
26
+ [HEADER_NAME, make_header_value(ALL_TYPES)]
28
27
  end
29
28
  end
30
29
 
@@ -36,16 +35,20 @@ module SecureHeaders
36
35
  unless config.all? { |t| t.is_a?(String) }
37
36
  raise ClearSiteDataConfigError.new("types must be Strings")
38
37
  end
39
-
40
- begin
41
- JSON.dump(config)
42
- rescue JSON::GeneratorError, Encoding::UndefinedConversionError
43
- raise ClearSiteDataConfigError.new("types must serializable by JSON")
44
- end
45
38
  else
46
39
  raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
47
40
  end
48
41
  end
42
+
43
+ # Public: Transform a Clear-Site-Data config (an Array of Strings) into a
44
+ # String that can be used as the value for the Clear-Site-Data header.
45
+ #
46
+ # types - An Array of String of types of data to clear.
47
+ #
48
+ # Returns a String of quoted values that are comma separated.
49
+ def make_header_value(types)
50
+ types.map { |t| "\"#{t}\""}.join(", ")
51
+ end
49
52
  end
50
53
  end
51
54
  end
@@ -15,6 +15,30 @@ module SecureHeaders
15
15
  end
16
16
 
17
17
  def initialize(hash)
18
+ @base_uri = nil
19
+ @block_all_mixed_content = nil
20
+ @child_src = nil
21
+ @connect_src = nil
22
+ @default_src = nil
23
+ @font_src = nil
24
+ @form_action = nil
25
+ @frame_ancestors = nil
26
+ @frame_src = nil
27
+ @img_src = nil
28
+ @manifest_src = nil
29
+ @media_src = nil
30
+ @object_src = nil
31
+ @plugin_types = nil
32
+ @preserve_schemes = nil
33
+ @report_only = nil
34
+ @report_uri = nil
35
+ @sandbox = nil
36
+ @script_nonce = nil
37
+ @script_src = nil
38
+ @style_nonce = nil
39
+ @style_src = nil
40
+ @upgrade_insecure_requests = nil
41
+
18
42
  from_hash(hash)
19
43
  @modified = false
20
44
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ExpectCertificateTransparencyConfigError < StandardError; end
4
+
5
+ class ExpectCertificateTransparency
6
+ HEADER_NAME = "Expect-CT".freeze
7
+ CONFIG_KEY = :expect_certificate_transparency
8
+ INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
+ INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
+ REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
11
+ INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
12
+
13
+ class << self
14
+ # Public: Generate a Expect-CT header.
15
+ #
16
+ # Returns nil if not configured, returns header name and value if
17
+ # configured.
18
+ def make_header(config)
19
+ return if config.nil?
20
+
21
+ header = new(config)
22
+ [HEADER_NAME, header.value]
23
+ end
24
+
25
+ def validate_config!(config)
26
+ return if config.nil? || config == OPT_OUT
27
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
28
+
29
+ unless [true, false, nil].include?(config[:enforce])
30
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
31
+ end
32
+
33
+ if !config[:max_age]
34
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
35
+ elsif config[:max_age].to_s !~ /\A\d+\z/
36
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(config)
42
+ @enforced = config.fetch(:enforce, nil)
43
+ @max_age = config.fetch(:max_age, nil)
44
+ @report_uri = config.fetch(:report_uri, nil)
45
+ end
46
+
47
+ def value
48
+ header_value = [
49
+ enforced_directive,
50
+ max_age_directive,
51
+ report_uri_directive
52
+ ].compact.join("; ").strip
53
+ end
54
+
55
+ def enforced_directive
56
+ # Unfortunately `if @enforced` isn't enough here in case someone
57
+ # passes in a random string so let's be specific with it to prevent
58
+ # accidental enforcement.
59
+ "enforce" if @enforced == true
60
+ end
61
+
62
+ def max_age_directive
63
+ "max-age=#{@max_age}" if @max_age
64
+ end
65
+
66
+ def report_uri_directive
67
+ "report-uri=\"#{@report_uri}\"" if @report_uri
68
+ end
69
+ end
70
+ end
@@ -62,22 +62,15 @@ module SecureHeaders
62
62
 
63
63
  # All the directives currently under consideration for CSP level 3.
64
64
  # https://w3c.github.io/webappsec/specs/CSP2/
65
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
65
66
  MANIFEST_SRC = :manifest_src
66
- REFLECTED_XSS = :reflected_xss
67
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
67
68
  DIRECTIVES_3_0 = [
68
69
  DIRECTIVES_2_0,
69
- MANIFEST_SRC,
70
- REFLECTED_XSS
71
- ].flatten.freeze
72
-
73
- # All the directives that are not currently in a formal spec, but have
74
- # been implemented somewhere.
75
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
76
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
77
- DIRECTIVES_DRAFT = [
78
70
  BLOCK_ALL_MIXED_CONTENT,
71
+ MANIFEST_SRC,
79
72
  UPGRADE_INSECURE_REQUESTS
80
- ].freeze
73
+ ].flatten.freeze
81
74
 
82
75
  EDGE_DIRECTIVES = DIRECTIVES_1_0
83
76
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
@@ -99,18 +92,18 @@ module SecureHeaders
99
92
  ].freeze
100
93
 
101
94
  FIREFOX_DIRECTIVES = (
102
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
95
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
103
96
  ).freeze
104
97
 
105
98
  FIREFOX_46_DIRECTIVES = (
106
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
99
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
107
100
  ).freeze
108
101
 
109
102
  CHROME_DIRECTIVES = (
110
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
103
+ DIRECTIVES_3_0
111
104
  ).freeze
112
105
 
113
- ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort
106
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
114
107
 
115
108
  # Think of default-src and report-uri as the beginning and end respectively,
116
109
  # everything else is in between.
@@ -156,7 +149,6 @@ module SecureHeaders
156
149
  MEDIA_SRC => :source_list,
157
150
  OBJECT_SRC => :source_list,
158
151
  PLUGIN_TYPES => :source_list,
159
- REFLECTED_XSS => :string,
160
152
  REPORT_URI => :source_list,
161
153
  SANDBOX => :source_list,
162
154
  SCRIPT_SRC => :source_list,
@@ -348,9 +340,9 @@ module SecureHeaders
348
340
  end
349
341
 
350
342
  def ensure_valid_sources!(directive, source_expression)
351
- source_expression.each do |source_expression|
352
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
353
- raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
343
+ source_expression.each do |expression|
344
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
345
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
354
346
  end
355
347
  end
356
348
  end
@@ -49,7 +49,7 @@ module SecureHeaders
49
49
  end
50
50
 
51
51
  def value
52
- header_value = [
52
+ [
53
53
  max_age_directive,
54
54
  pin_directives,
55
55
  report_uri_directive,
@@ -95,9 +95,9 @@ module SecureHeaders
95
95
  <<-EOF
96
96
  \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
97
97
  #{content}
98
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
98
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
99
99
  #{file_path}:
100
- - #{hash_value}\n\n
100
+ - \"#{hash_value}\"\n\n
101
101
  NOTE: dynamic javascript is not supported using script hash integration
102
102
  on purpose. It defeats the point of using it in the first place.
103
103
  EOF
@@ -11,6 +11,7 @@ require "secure_headers/headers/x_download_options"
11
11
  require "secure_headers/headers/x_permitted_cross_domain_policies"
12
12
  require "secure_headers/headers/referrer_policy"
13
13
  require "secure_headers/headers/clear_site_data"
14
+ require "secure_headers/headers/expect_certificate_transparency"
14
15
  require "secure_headers/middleware"
15
16
  require "secure_headers/railtie"
16
17
  require "secure_headers/view_helper"
@@ -51,6 +52,7 @@ module SecureHeaders
51
52
  CSP = ContentSecurityPolicy
52
53
 
53
54
  ALL_HEADER_CLASSES = [
55
+ ExpectCertificateTransparency,
54
56
  ClearSiteData,
55
57
  ContentSecurityPolicyConfig,
56
58
  ContentSecurityPolicyReportOnlyConfig,
@@ -163,7 +165,8 @@ module SecureHeaders
163
165
  # returned is meant to be merged into the header value from `@app.call(env)`
164
166
  # in Rack middleware.
165
167
  def header_hash_for(request)
166
- config = config_for(request, prevent_dup = true)
168
+ prevent_dup = true
169
+ config = config_for(request, prevent_dup)
167
170
  headers = config.cached_headers
168
171
  user_agent = UserAgent.parse(request.user_agent)
169
172
 
@@ -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.6.4"
4
+ gem.version = "3.7.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Manages application of security headers with many safe defaults.'
@@ -19,30 +19,16 @@ module SecureHeaders
19
19
  name, value = described_class.make_header(true)
20
20
 
21
21
  expect(name).to eq(ClearSiteData::HEADER_NAME)
22
- expect(value).to eq(normalize_json(<<-HERE))
23
- {
24
- "types": [
25
- "cache",
26
- "cookies",
27
- "storage",
28
- "executionContexts"
29
- ]
30
- }
31
- HERE
22
+ expect(value).to eq(
23
+ %("cache", "cookies", "storage", "executionContexts")
24
+ )
32
25
  end
33
26
 
34
27
  it "returns specified types" do
35
28
  name, value = described_class.make_header(["foo", "bar"])
36
29
 
37
30
  expect(name).to eq(ClearSiteData::HEADER_NAME)
38
- expect(value).to eq(normalize_json(<<-HERE))
39
- {
40
- "types": [
41
- "foo",
42
- "bar"
43
- ]
44
- }
45
- HERE
31
+ expect(value).to eq(%("foo", "bar"))
46
32
  end
47
33
  end
48
34
 
@@ -83,12 +69,6 @@ module SecureHeaders
83
69
  end.to raise_error(ClearSiteDataConfigError)
84
70
  end
85
71
 
86
- it "fails for non-serializable config" do
87
- expect do
88
- described_class.validate_config!(["hi \255"])
89
- end.to raise_error(ClearSiteDataConfigError)
90
- end
91
-
92
72
  it "fails for other types of config" do
93
73
  expect do
94
74
  described_class.validate_config!(:cookies)
@@ -96,8 +76,11 @@ module SecureHeaders
96
76
  end
97
77
  end
98
78
 
99
- def normalize_json(json)
100
- JSON.dump(JSON.parse(json))
79
+ describe "make_header_value" do
80
+ it "returns a string of quoted values that are comma separated" do
81
+ value = described_class.make_header_value(["foo", "bar"])
82
+ expect(value).to eq(%("foo", "bar"))
83
+ end
101
84
  end
102
85
  end
103
86
  end
@@ -119,7 +119,6 @@ module SecureHeaders
119
119
  end.merge({
120
120
  block_all_mixed_content: true,
121
121
  upgrade_insecure_requests: true,
122
- reflected_xss: "block",
123
122
  script_src: %w(script-src.com),
124
123
  script_nonce: 123456
125
124
  })
@@ -127,22 +126,22 @@ module SecureHeaders
127
126
 
128
127
  it "does not filter any directives for Chrome" do
129
128
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
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")
129
+ 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; manifest-src manifest-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")
131
130
  end
132
131
 
133
132
  it "does not filter any directives for Opera" do
134
133
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
135
- 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")
134
+ 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; manifest-src manifest-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")
136
135
  end
137
136
 
138
137
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
139
138
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
140
- 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")
139
+ 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; manifest-src manifest-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")
141
140
  end
142
141
 
143
142
  it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
144
143
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
145
- 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")
144
+ 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; manifest-src manifest-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")
146
145
  end
147
146
 
148
147
  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
@@ -164,7 +163,7 @@ module SecureHeaders
164
163
  ua = USER_AGENTS[:firefox].dup
165
164
  allow(ua).to receive(:version).and_return(nil)
166
165
  policy = ContentSecurityPolicy.new(complex_opts, ua)
167
- 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")
166
+ 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; manifest-src manifest-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")
168
167
  end
169
168
  end
170
169
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ module SecureHeaders
5
+ describe ExpectCertificateTransparency do
6
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
7
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
8
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
9
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") }
10
+ specify do
11
+ config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
12
+ header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
13
+ expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
14
+ end
15
+
16
+ context "with an invalid configuration" do
17
+ it "raises an exception when configuration isn't a hash" do
18
+ expect do
19
+ ExpectCertificateTransparency.validate_config!(%w(a))
20
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
21
+ end
22
+
23
+ it "raises an exception when max-age is not provided" do
24
+ expect do
25
+ ExpectCertificateTransparency.validate_config!(foo: "bar")
26
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
27
+ end
28
+
29
+ it "raises an exception with an invalid max-age" do
30
+ expect do
31
+ ExpectCertificateTransparency.validate_config!(max_age: "abc123")
32
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
33
+ end
34
+
35
+ it "raises an exception with an invalid enforce value" do
36
+ expect do
37
+ ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
38
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -27,6 +27,7 @@ module SecureHeaders
27
27
  connect_src: %w(wss:),
28
28
  font_src: %w('self' data:),
29
29
  img_src: %w(mycdn.com data:),
30
+ manifest_src: %w(manifest.com),
30
31
  media_src: %w(utoob.com),
31
32
  object_src: %w('self'),
32
33
  script_src: %w('self'),
@@ -37,7 +37,6 @@ class Message < ERB
37
37
  background-color: black;
38
38
  }
39
39
  </style>
40
- <%= @name %>
41
40
 
42
41
  TEMPLATE
43
42
  end
@@ -105,7 +105,7 @@ module SecureHeaders
105
105
  end
106
106
 
107
107
  it "produces a UA-specific CSP when overriding (and busting the cache)" do
108
- config = Configuration.default do |config|
108
+ Configuration.default do |config|
109
109
  config.csp = {
110
110
  default_src: %w('self'),
111
111
  child_src: %w('self')
@@ -231,7 +231,7 @@ module SecureHeaders
231
231
 
232
232
  SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
233
233
  hash = SecureHeaders.header_hash_for(chrome_request)
234
- expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
234
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/)
235
235
  end
236
236
 
237
237
  it "appends a hash to a missing script-src value" do
@@ -243,7 +243,7 @@ module SecureHeaders
243
243
 
244
244
  SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
245
245
  hash = SecureHeaders.header_hash_for(chrome_request)
246
- expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
246
+ expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/)
247
247
  end
248
248
 
249
249
  it "overrides individual directives" do
@@ -279,7 +279,7 @@ module SecureHeaders
279
279
  end
280
280
 
281
281
  safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
282
- nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
282
+ SecureHeaders.content_security_policy_script_nonce(safari_request)
283
283
  hash = SecureHeaders.header_hash_for(safari_request)
284
284
  expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
285
285
  end
data/spec/spec_helper.rb CHANGED
@@ -34,6 +34,7 @@ def expect_default_values(hash)
34
34
  expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
35
35
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
36
36
  expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
37
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
37
38
  end
38
39
 
39
40
  module SecureHeaders
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.6.4
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-03 00:00:00.000000000 Z
11
+ date: 2017-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -73,6 +73,7 @@ files:
73
73
  - lib/secure_headers/headers/content_security_policy.rb
74
74
  - lib/secure_headers/headers/content_security_policy_config.rb
75
75
  - lib/secure_headers/headers/cookie.rb
76
+ - lib/secure_headers/headers/expect_certificate_transparency.rb
76
77
  - lib/secure_headers/headers/policy_management.rb
77
78
  - lib/secure_headers/headers/public_key_pins.rb
78
79
  - lib/secure_headers/headers/referrer_policy.rb
@@ -92,6 +93,7 @@ files:
92
93
  - spec/lib/secure_headers/headers/clear_site_data_spec.rb
93
94
  - spec/lib/secure_headers/headers/content_security_policy_spec.rb
94
95
  - spec/lib/secure_headers/headers/cookie_spec.rb
96
+ - spec/lib/secure_headers/headers/expect_certificate_spec.rb
95
97
  - spec/lib/secure_headers/headers/policy_management_spec.rb
96
98
  - spec/lib/secure_headers/headers/public_key_pins_spec.rb
97
99
  - spec/lib/secure_headers/headers/referrer_policy_spec.rb
@@ -136,6 +138,7 @@ test_files:
136
138
  - spec/lib/secure_headers/headers/clear_site_data_spec.rb
137
139
  - spec/lib/secure_headers/headers/content_security_policy_spec.rb
138
140
  - spec/lib/secure_headers/headers/cookie_spec.rb
141
+ - spec/lib/secure_headers/headers/expect_certificate_spec.rb
139
142
  - spec/lib/secure_headers/headers/policy_management_spec.rb
140
143
  - spec/lib/secure_headers/headers/public_key_pins_spec.rb
141
144
  - spec/lib/secure_headers/headers/referrer_policy_spec.rb