secure_headers 3.2.0 → 4.0.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +8 -4
  8. data/CHANGELOG.md +144 -1
  9. data/CODE_OF_CONDUCT.md +46 -0
  10. data/CONTRIBUTING.md +41 -0
  11. data/Gemfile +9 -4
  12. data/Guardfile +1 -0
  13. data/LICENSE +4 -199
  14. data/README.md +73 -344
  15. data/Rakefile +22 -18
  16. data/docs/HPKP.md +17 -0
  17. data/docs/cookies.md +64 -0
  18. data/docs/hashes.md +64 -0
  19. data/docs/named_overrides_and_appends.md +107 -0
  20. data/docs/per_action_configuration.md +133 -0
  21. data/docs/sinatra.md +25 -0
  22. data/lib/secure_headers/configuration.rb +116 -42
  23. data/lib/secure_headers/hash_helper.rb +2 -1
  24. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  25. data/lib/secure_headers/headers/content_security_policy.rb +128 -39
  26. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  27. data/lib/secure_headers/headers/cookie.rb +21 -3
  28. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  29. data/lib/secure_headers/headers/policy_management.rb +158 -68
  30. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  31. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  32. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  33. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  36. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  37. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  38. data/lib/secure_headers/middleware.rb +16 -9
  39. data/lib/secure_headers/railtie.rb +7 -6
  40. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  41. data/lib/secure_headers/view_helper.rb +15 -8
  42. data/lib/secure_headers.rb +139 -55
  43. data/lib/tasks/tasks.rake +7 -5
  44. data/secure_headers.gemspec +13 -3
  45. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  46. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  47. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
  48. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  49. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  50. data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
  51. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  52. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  53. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  54. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  56. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  57. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  58. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  59. data/spec/lib/secure_headers/middleware_spec.rb +55 -17
  60. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  61. data/spec/lib/secure_headers_spec.rb +330 -58
  62. data/spec/spec_helper.rb +17 -25
  63. data/upgrading-to-3-0.md +13 -10
  64. data/upgrading-to-4-0.md +55 -0
  65. metadata +34 -7
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe PublicKeyPins do
@@ -8,7 +9,7 @@ module SecureHeaders
8
9
  specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
9
10
  specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
10
11
  specify do
11
- config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] }
12
+ config = { max_age: 1234, pins: [{ sha256: "base64encodedpin1" }, { sha256: "base64encodedpin2" }] }
12
13
  header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
13
14
  expect(PublicKeyPins.new(config).value).to eq(header_value)
14
15
  end
@@ -16,19 +17,19 @@ module SecureHeaders
16
17
  context "with an invalid configuration" do
17
18
  it "raises an exception when max-age is not provided" do
18
19
  expect do
19
- PublicKeyPins.validate_config!(foo: 'bar')
20
+ PublicKeyPins.validate_config!(foo: "bar")
20
21
  end.to raise_error(PublicKeyPinsConfigError)
21
22
  end
22
23
 
23
24
  it "raises an exception with an invalid max-age" do
24
25
  expect do
25
- PublicKeyPins.validate_config!(max_age: 'abc123')
26
+ PublicKeyPins.validate_config!(max_age: "abc123")
26
27
  end.to raise_error(PublicKeyPinsConfigError)
27
28
  end
28
29
 
29
- it 'raises an exception with less than 2 pins' do
30
+ it "raises an exception with less than 2 pins" do
30
31
  expect do
31
- config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] }
32
+ config = { max_age: 1234, pins: [{ sha256: "base64encodedpin" }] }
32
33
  PublicKeyPins.validate_config!(config)
33
34
  end.to raise_error(PublicKeyPinsConfigError)
34
35
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ module SecureHeaders
5
+ describe ReferrerPolicy do
6
+ specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) }
7
+ specify { expect(ReferrerPolicy.make_header("no-referrer")).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) }
8
+
9
+ context "valid configuration values" do
10
+ it "accepts 'no-referrer'" do
11
+ expect do
12
+ ReferrerPolicy.validate_config!("no-referrer")
13
+ end.not_to raise_error
14
+ end
15
+
16
+ it "accepts 'no-referrer-when-downgrade'" do
17
+ expect do
18
+ ReferrerPolicy.validate_config!("no-referrer-when-downgrade")
19
+ end.not_to raise_error
20
+ end
21
+
22
+ it "accepts 'same-origin'" do
23
+ expect do
24
+ ReferrerPolicy.validate_config!("same-origin")
25
+ end.not_to raise_error
26
+ end
27
+
28
+ it "accepts 'strict-origin'" do
29
+ expect do
30
+ ReferrerPolicy.validate_config!("strict-origin")
31
+ end.not_to raise_error
32
+ end
33
+
34
+ it "accepts 'strict-origin-when-cross-origin'" do
35
+ expect do
36
+ ReferrerPolicy.validate_config!("strict-origin-when-cross-origin")
37
+ end.not_to raise_error
38
+ end
39
+
40
+ it "accepts 'origin'" do
41
+ expect do
42
+ ReferrerPolicy.validate_config!("origin")
43
+ end.not_to raise_error
44
+ end
45
+
46
+ it "accepts 'origin-when-cross-origin'" do
47
+ expect do
48
+ ReferrerPolicy.validate_config!("origin-when-cross-origin")
49
+ end.not_to raise_error
50
+ end
51
+
52
+ it "accepts 'unsafe-url'" do
53
+ expect do
54
+ ReferrerPolicy.validate_config!("unsafe-url")
55
+ end.not_to raise_error
56
+ end
57
+
58
+ it "accepts nil" do
59
+ expect do
60
+ ReferrerPolicy.validate_config!(nil)
61
+ end.not_to raise_error
62
+ end
63
+ end
64
+
65
+ context "invlaid configuration values" do
66
+ it "doesn't accept invalid values" do
67
+ expect do
68
+ ReferrerPolicy.validate_config!("open")
69
+ end.to raise_error(ReferrerPolicyConfigError)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe StrictTransportSecurity do
@@ -10,19 +11,19 @@ module SecureHeaders
10
11
  context "with a string argument" do
11
12
  it "raises an exception with an invalid max-age" do
12
13
  expect do
13
- StrictTransportSecurity.validate_config!('max-age=abc123')
14
+ StrictTransportSecurity.validate_config!("max-age=abc123")
14
15
  end.to raise_error(STSConfigError)
15
16
  end
16
17
 
17
18
  it "raises an exception if max-age is not supplied" do
18
19
  expect do
19
- StrictTransportSecurity.validate_config!('includeSubdomains')
20
+ StrictTransportSecurity.validate_config!("includeSubdomains")
20
21
  end.to raise_error(STSConfigError)
21
22
  end
22
23
 
23
24
  it "raises an exception with an invalid format" do
24
25
  expect do
25
- StrictTransportSecurity.validate_config!('max-age=123includeSubdomains')
26
+ StrictTransportSecurity.validate_config!("max-age=123includeSubdomains")
26
27
  end.to raise_error(STSConfigError)
27
28
  end
28
29
  end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe XContentTypeOptions do
@@ -1,9 +1,10 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe XDownloadOptions do
5
6
  specify { expect(XDownloadOptions.make_header).to eq([XDownloadOptions::HEADER_NAME, XDownloadOptions::DEFAULT_VALUE]) }
6
- specify { expect(XDownloadOptions.make_header('noopen')).to eq([XDownloadOptions::HEADER_NAME, 'noopen']) }
7
+ specify { expect(XDownloadOptions.make_header("noopen")).to eq([XDownloadOptions::HEADER_NAME, "noopen"]) }
7
8
 
8
9
  context "invalid configuration values" do
9
10
  it "accepts noopen" do
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe XFrameOptions do
@@ -1,9 +1,10 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe XPermittedCrossDomainPolicies do
5
6
  specify { expect(XPermittedCrossDomainPolicies.make_header).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "none"]) }
6
- specify { expect(XPermittedCrossDomainPolicies.make_header('master-only')).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, 'master-only']) }
7
+ specify { expect(XPermittedCrossDomainPolicies.make_header("master-only")).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "master-only"]) }
7
8
 
8
9
  context "valid configuration values" do
9
10
  it "accepts 'all'" do
@@ -36,7 +37,7 @@ module SecureHeaders
36
37
  end
37
38
  end
38
39
 
39
- context 'invlaid configuration values' do
40
+ context "invlaid configuration values" do
40
41
  it "doesn't accept invalid values" do
41
42
  expect do
42
43
  XPermittedCrossDomainPolicies.validate_config!("open")
@@ -1,9 +1,10 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe XXssProtection do
5
6
  specify { expect(XXssProtection.make_header).to eq([XXssProtection::HEADER_NAME, XXssProtection::DEFAULT_VALUE]) }
6
- specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, '1; mode=block; report=https://www.secure.com/reports']) }
7
+ specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, "1; mode=block; report=https://www.secure.com/reports"]) }
7
8
 
8
9
  context "with invalid configuration" do
9
10
  it "should raise an error when providing a string that is not valid" do
@@ -19,7 +20,7 @@ module SecureHeaders
19
20
  context "when using a hash value" do
20
21
  it "should allow string values ('1' or '0' are the only valid strings)" do
21
22
  expect do
22
- XXssProtection.validate_config!('1')
23
+ XXssProtection.validate_config!("1")
23
24
  end.not_to raise_error
24
25
  end
25
26
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "spec_helper"
2
3
 
3
4
  module SecureHeaders
@@ -13,6 +14,24 @@ module SecureHeaders
13
14
  Configuration.default
14
15
  end
15
16
 
17
+ it "warns if the hpkp report-uri host is the same as the current host" do
18
+ report_host = "report-uri.io"
19
+ Configuration.default do |config|
20
+ config.hpkp = {
21
+ max_age: 10000000,
22
+ pins: [
23
+ {sha256: "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
24
+ {sha256: "73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f"}
25
+ ],
26
+ report_uri: "https://#{report_host}/example-hpkp"
27
+ }
28
+ end
29
+
30
+ expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING)
31
+
32
+ middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {}))
33
+ end
34
+
16
35
  it "sets the headers" do
17
36
  _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {}))
18
37
  expect_default_values(env)
@@ -33,59 +52,78 @@ module SecureHeaders
33
52
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
34
53
  expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
35
54
  _, env = middleware.call request.env
36
- expect(env[CSP::HEADER_NAME]).to match("example.org")
55
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
37
56
  end
38
57
 
39
- context "secure_cookies" do
58
+ context "cookies" do
40
59
  context "cookies should be flagged" do
41
60
  it "flags cookies as secure" do
42
- capture_warning do
43
- Configuration.default { |config| config.secure_cookies = true }
44
- end
61
+ Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} }
45
62
  request = Rack::Request.new("HTTPS" => "on")
46
63
  _, env = cookie_middleware.call request.env
47
- expect(env['Set-Cookie']).to eq("foo=bar; secure")
64
+ expect(env["Set-Cookie"]).to eq("foo=bar; secure")
48
65
  end
49
66
  end
50
67
 
68
+ it "allows opting out of cookie protection with OPT_OUT alone" do
69
+ Configuration.default { |config| config.cookies = OPT_OUT}
70
+
71
+ # do NOT make this request https. non-https requests modify a config,
72
+ # causing an exception when operating on OPT_OUT. This ensures we don't
73
+ # try to modify the config.
74
+ request = Rack::Request.new({})
75
+ _, env = cookie_middleware.call request.env
76
+ expect(env["Set-Cookie"]).to eq("foo=bar")
77
+ end
78
+
51
79
  context "cookies should not be flagged" do
52
80
  it "does not flags cookies as secure" do
53
- capture_warning do
54
- Configuration.default { |config| config.secure_cookies = false }
55
- end
81
+ Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} }
56
82
  request = Rack::Request.new("HTTPS" => "on")
57
83
  _, env = cookie_middleware.call request.env
58
- expect(env['Set-Cookie']).to eq("foo=bar")
84
+ expect(env["Set-Cookie"]).to eq("foo=bar")
59
85
  end
60
86
  end
61
87
  end
62
88
 
63
89
  context "cookies" do
64
90
  it "flags cookies from configuration" do
65
- Configuration.default { |config| config.cookies = { secure: true, httponly: true } }
91
+ Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } }
66
92
  request = Rack::Request.new("HTTPS" => "on")
67
93
  _, env = cookie_middleware.call request.env
68
94
 
69
- expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly")
95
+ expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly; SameSite=Lax")
70
96
  end
71
97
 
72
98
  it "flags cookies with a combination of SameSite configurations" do
73
99
  cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })
74
100
 
75
- Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } }
101
+ Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} }
76
102
  request = Rack::Request.new("HTTPS" => "on")
77
103
  _, env = cookie_middleware.call request.env
78
104
 
79
- expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict")
80
- expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax")
105
+ expect(env["Set-Cookie"]).to match("_session=foobar; SameSite=Strict")
106
+ expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax")
81
107
  end
82
108
 
83
109
  it "disables secure cookies for non-https requests" do
84
- Configuration.default { |config| config.cookies = { secure: true } }
110
+ Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } }
85
111
 
86
112
  request = Rack::Request.new("HTTPS" => "off")
87
113
  _, env = cookie_middleware.call request.env
88
- expect(env['Set-Cookie']).to eq("foo=bar")
114
+ expect(env["Set-Cookie"]).to eq("foo=bar")
115
+ end
116
+
117
+ it "sets the secure cookie flag correctly on interleaved http/https requests" do
118
+ Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } }
119
+
120
+ request = Rack::Request.new("HTTPS" => "off")
121
+ _, env = cookie_middleware.call request.env
122
+ expect(env["Set-Cookie"]).to eq("foo=bar")
123
+
124
+ request = Rack::Request.new("HTTPS" => "on")
125
+ _, env = cookie_middleware.call request.env
126
+ expect(env["Set-Cookie"]).to eq("foo=bar; secure")
89
127
  end
90
128
  end
91
129
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "spec_helper"
2
3
  require "erb"
3
4
 
@@ -27,7 +28,16 @@ class Message < ERB
27
28
  background-color: black;
28
29
  }
29
30
  <% end %>
30
- <%= @name %>
31
+
32
+ <script nonce="<%= content_security_policy_script_nonce %>">
33
+ alert(1)
34
+ </script>
35
+
36
+ <style nonce="<%= content_security_policy_style_nonce %>">
37
+ body {
38
+ background-color: black;
39
+ }
40
+ </style>
31
41
 
32
42
  TEMPLATE
33
43
  end
@@ -49,7 +59,7 @@ TEMPLATE
49
59
  end
50
60
 
51
61
  if options.is_a?(Hash)
52
- options = options.map {|k,v| " #{k}=#{v}"}
62
+ options = options.map {|k, v| " #{k}=#{v}"}
53
63
  end
54
64
  "<#{type}#{options}>#{content}</#{type}>"
55
65
  end
@@ -72,8 +82,11 @@ module SecureHeaders
72
82
 
73
83
  before(:all) do
74
84
  Configuration.default do |config|
75
- config.csp[:script_src] = %w('self')
76
- config.csp[:style_src] = %w('self')
85
+ config.csp = {
86
+ default_src: %w('self'),
87
+ script_src: %w('self'),
88
+ style_src: %w('self')
89
+ }
77
90
  end
78
91
  end
79
92
 
@@ -115,10 +128,10 @@ module SecureHeaders
115
128
  Message.new(request).result
116
129
  _, env = middleware.call request.env
117
130
 
118
- expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
119
- expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
120
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
121
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
131
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
132
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
133
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
134
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
122
135
  end
123
136
  end
124
137
  end