secure_headers 3.0.3 → 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 (68) 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/.gitignore +0 -9
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +3 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +17 -5
  9. data/CHANGELOG.md +287 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +41 -0
  12. data/Gemfile +13 -5
  13. data/Guardfile +1 -0
  14. data/LICENSE +4 -199
  15. data/README.md +85 -265
  16. data/Rakefile +22 -18
  17. data/docs/HPKP.md +17 -0
  18. data/docs/cookies.md +64 -0
  19. data/docs/hashes.md +64 -0
  20. data/docs/named_overrides_and_appends.md +107 -0
  21. data/docs/per_action_configuration.md +133 -0
  22. data/docs/sinatra.md +25 -0
  23. data/lib/secure_headers/configuration.rb +188 -59
  24. data/lib/secure_headers/hash_helper.rb +11 -0
  25. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  26. data/lib/secure_headers/headers/content_security_policy.rb +136 -325
  27. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  28. data/lib/secure_headers/headers/cookie.rb +144 -0
  29. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  30. data/lib/secure_headers/headers/policy_management.rb +410 -0
  31. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  32. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  33. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  34. data/lib/secure_headers/headers/x_content_type_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  36. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  37. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  38. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  39. data/lib/secure_headers/middleware.rb +44 -0
  40. data/lib/secure_headers/railtie.rb +11 -6
  41. data/lib/secure_headers/utils/cookies_config.rb +95 -0
  42. data/lib/secure_headers/view_helper.rb +76 -5
  43. data/lib/secure_headers.rb +159 -99
  44. data/lib/tasks/tasks.rake +83 -0
  45. data/secure_headers.gemspec +13 -3
  46. data/spec/lib/secure_headers/configuration_spec.rb +24 -9
  47. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  48. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
  49. data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
  50. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  51. data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
  52. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  53. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  54. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +6 -5
  55. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  56. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  57. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  58. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  59. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  60. data/spec/lib/secure_headers/middleware_spec.rb +96 -6
  61. data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
  62. data/spec/lib/secure_headers_spec.rb +407 -58
  63. data/spec/spec_helper.rb +19 -12
  64. data/upgrading-to-3-0.md +13 -9
  65. data/upgrading-to-4-0.md +55 -0
  66. metadata +45 -9
  67. data/lib/secure_headers/padrino.rb +0 -13
  68. data/travis.sh +0 -10
@@ -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,18 +1,35 @@
1
+ # frozen_string_literal: true
1
2
  require "spec_helper"
2
3
 
3
4
  module SecureHeaders
4
5
  describe Middleware do
5
- let(:app) { ->(env) { [200, env, "app"] } }
6
+ let(:app) { lambda { |env| [200, env, "app"] } }
7
+ let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } }
6
8
 
7
- let :middleware do
8
- Middleware.new(app)
9
- end
9
+ let(:middleware) { Middleware.new(app) }
10
+ let(:cookie_middleware) { Middleware.new(cookie_app) }
10
11
 
11
12
  before(:each) do
12
13
  reset_config
14
+ Configuration.default
15
+ end
16
+
17
+ it "warns if the hpkp report-uri host is the same as the current host" do
18
+ report_host = "report-uri.io"
13
19
  Configuration.default do |config|
14
- # use all default provided by the library
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
+ }
15
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}", {}))
16
33
  end
17
34
 
18
35
  it "sets the headers" do
@@ -33,8 +50,81 @@ module SecureHeaders
33
50
  end
34
51
  request = Rack::Request.new({})
35
52
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
53
+ expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
36
54
  _, env = middleware.call request.env
37
- expect(env[CSP::HEADER_NAME]).to match("example.org")
55
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
56
+ end
57
+
58
+ context "cookies" do
59
+ context "cookies should be flagged" do
60
+ it "flags cookies as secure" do
61
+ Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} }
62
+ request = Rack::Request.new("HTTPS" => "on")
63
+ _, env = cookie_middleware.call request.env
64
+ expect(env["Set-Cookie"]).to eq("foo=bar; secure")
65
+ end
66
+ end
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
+
79
+ context "cookies should not be flagged" do
80
+ it "does not flags cookies as secure" do
81
+ Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} }
82
+ request = Rack::Request.new("HTTPS" => "on")
83
+ _, env = cookie_middleware.call request.env
84
+ expect(env["Set-Cookie"]).to eq("foo=bar")
85
+ end
86
+ end
87
+ end
88
+
89
+ context "cookies" do
90
+ it "flags cookies from configuration" do
91
+ Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } }
92
+ request = Rack::Request.new("HTTPS" => "on")
93
+ _, env = cookie_middleware.call request.env
94
+
95
+ expect(env["Set-Cookie"]).to eq("foo=bar; secure; HttpOnly; SameSite=Lax")
96
+ end
97
+
98
+ it "flags cookies with a combination of SameSite configurations" do
99
+ cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })
100
+
101
+ Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } }, httponly: OPT_OUT, secure: OPT_OUT} }
102
+ request = Rack::Request.new("HTTPS" => "on")
103
+ _, env = cookie_middleware.call request.env
104
+
105
+ expect(env["Set-Cookie"]).to match("_session=foobar; SameSite=Strict")
106
+ expect(env["Set-Cookie"]).to match("_guest=true; SameSite=Lax")
107
+ end
108
+
109
+ it "disables secure cookies for non-https requests" do
110
+ Configuration.default { |config| config.cookies = { secure: true, httponly: OPT_OUT, samesite: OPT_OUT } }
111
+
112
+ request = Rack::Request.new("HTTPS" => "off")
113
+ _, env = cookie_middleware.call request.env
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")
127
+ end
38
128
  end
39
129
  end
40
130
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require "erb"
4
+
5
+ class Message < ERB
6
+ include SecureHeaders::ViewHelpers
7
+
8
+ def self.template
9
+ <<-TEMPLATE
10
+ <% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %>
11
+ console.log(1)
12
+ <% end %>
13
+
14
+ <% hashed_style_tag do %>
15
+ body {
16
+ background-color: black;
17
+ }
18
+ <% end %>
19
+
20
+ <% nonced_javascript_tag do %>
21
+ body {
22
+ console.log(1)
23
+ }
24
+ <% end %>
25
+
26
+ <% nonced_style_tag do %>
27
+ body {
28
+ background-color: black;
29
+ }
30
+ <% end %>
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>
41
+
42
+ TEMPLATE
43
+ end
44
+
45
+ def initialize(request, options = {})
46
+ @virtual_path = "/asdfs/index"
47
+ @_request = request
48
+ @template = self.class.template
49
+ super(@template)
50
+ end
51
+
52
+ def capture(*args)
53
+ yield(*args)
54
+ end
55
+
56
+ def content_tag(type, content = nil, options = nil, &block)
57
+ content = if block_given?
58
+ capture(block)
59
+ end
60
+
61
+ if options.is_a?(Hash)
62
+ options = options.map {|k, v| " #{k}=#{v}"}
63
+ end
64
+ "<#{type}#{options}>#{content}</#{type}>"
65
+ end
66
+
67
+ def result
68
+ super(binding)
69
+ end
70
+
71
+ def request
72
+ @_request
73
+ end
74
+ end
75
+
76
+ module SecureHeaders
77
+ describe ViewHelpers do
78
+ let(:app) { lambda { |env| [200, env, "app"] } }
79
+ let(:middleware) { Middleware.new(app) }
80
+ let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) }
81
+ let(:filename) { "app/views/asdfs/index.html.erb" }
82
+
83
+ before(:all) do
84
+ Configuration.default do |config|
85
+ config.csp = {
86
+ default_src: %w('self'),
87
+ script_src: %w('self'),
88
+ style_src: %w('self')
89
+ }
90
+ end
91
+ end
92
+
93
+ after(:each) do
94
+ Configuration.instance_variable_set(:@script_hashes, nil)
95
+ Configuration.instance_variable_set(:@style_hashes, nil)
96
+ end
97
+
98
+ it "raises an error when using hashed content without precomputed hashes" do
99
+ expect {
100
+ Message.new(request).result
101
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
102
+ end
103
+
104
+ it "raises an error when using hashed content with precomputed hashes, but none for the given file" do
105
+ Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"])
106
+ expect {
107
+ Message.new(request).result
108
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
109
+ end
110
+
111
+ it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do
112
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"])
113
+ expect {
114
+ Message.new(request).result
115
+ }.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
116
+ end
117
+
118
+ it "adds known hash values to the corresponding headers when the helper is used" do
119
+ begin
120
+ allow(SecureRandom).to receive(:base64).and_return("abc123")
121
+
122
+ expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
123
+ Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
124
+ expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
125
+ Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
126
+
127
+ # render erb that calls out to helpers.
128
+ Message.new(request).result
129
+ _, env = middleware.call request.env
130
+
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)}'/)
135
+ end
136
+ end
137
+ end
138
+ end