secure_headers 3.1.2 → 3.2.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.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -0
- data/README.md +144 -21
- data/lib/secure_headers/configuration.rb +19 -4
- data/lib/secure_headers/hash_helper.rb +10 -0
- data/lib/secure_headers/headers/cookie.rb +126 -0
- data/lib/secure_headers/middleware.rb +23 -9
- data/lib/secure_headers/railtie.rb +4 -0
- data/lib/secure_headers/utils/cookies_config.rb +94 -0
- data/lib/secure_headers/view_helper.rb +64 -0
- data/lib/secure_headers.rb +2 -0
- data/lib/tasks/tasks.rake +81 -0
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +9 -1
- data/spec/lib/secure_headers/headers/cookie_spec.rb +164 -0
- data/spec/lib/secure_headers/middleware_spec.rb +48 -15
- data/spec/lib/secure_headers/view_helpers_spec.rb +125 -0
- data/spec/spec_helper.rb +12 -0
- data/upgrading-to-3-0.md +1 -0
- metadata +10 -2
@@ -1,5 +1,10 @@
|
|
1
1
|
module SecureHeaders
|
2
2
|
module ViewHelpers
|
3
|
+
include SecureHeaders::HashHelper
|
4
|
+
SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
|
5
|
+
|
6
|
+
class UnexpectedHashedScriptException < StandardError; end
|
7
|
+
|
3
8
|
# Public: create a style tag using the content security policy nonce.
|
4
9
|
# Instructs secure_headers to append a nonce to style/script-src directives.
|
5
10
|
#
|
@@ -29,8 +34,67 @@ module SecureHeaders
|
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
37
|
+
##
|
38
|
+
# Checks to see if the hashed code is expected and adds the hash source
|
39
|
+
# value to the current CSP.
|
40
|
+
#
|
41
|
+
# By default, in development/test/etc. an exception will be raised.
|
42
|
+
def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block)
|
43
|
+
hashed_tag(
|
44
|
+
:script,
|
45
|
+
:script_src,
|
46
|
+
Configuration.instance_variable_get(:@script_hashes),
|
47
|
+
raise_error_on_unrecognized_hash,
|
48
|
+
block
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block)
|
53
|
+
hashed_tag(
|
54
|
+
:style,
|
55
|
+
:style_src,
|
56
|
+
Configuration.instance_variable_get(:@style_hashes),
|
57
|
+
raise_error_on_unrecognized_hash,
|
58
|
+
block
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
32
62
|
private
|
33
63
|
|
64
|
+
def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block)
|
65
|
+
if raise_error_on_unrecognized_hash.nil?
|
66
|
+
raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production"
|
67
|
+
end
|
68
|
+
|
69
|
+
content = capture(&block)
|
70
|
+
file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
|
71
|
+
|
72
|
+
if raise_error_on_unrecognized_hash
|
73
|
+
hash_value = hash_source(content)
|
74
|
+
message = unexpected_hash_error_message(file_path, content, hash_value)
|
75
|
+
|
76
|
+
if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value)
|
77
|
+
raise UnexpectedHashedScriptException.new(message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path])
|
82
|
+
|
83
|
+
content_tag type, content
|
84
|
+
end
|
85
|
+
|
86
|
+
def unexpected_hash_error_message(file_path, content, hash_value)
|
87
|
+
<<-EOF
|
88
|
+
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
89
|
+
#{content}
|
90
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
|
91
|
+
#{file_path}:
|
92
|
+
- #{hash_value}\n\n
|
93
|
+
NOTE: dynamic javascript is not supported using script hash integration
|
94
|
+
on purpose. It defeats the point of using it in the first place.
|
95
|
+
EOF
|
96
|
+
end
|
97
|
+
|
34
98
|
def nonced_tag(type, content_or_options, block)
|
35
99
|
options = {}
|
36
100
|
content = if block
|
data/lib/secure_headers.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require "secure_headers/configuration"
|
2
|
+
require "secure_headers/hash_helper"
|
3
|
+
require "secure_headers/headers/cookie"
|
2
4
|
require "secure_headers/headers/public_key_pins"
|
3
5
|
require "secure_headers/headers/content_security_policy"
|
4
6
|
require "secure_headers/headers/x_frame_options"
|
@@ -0,0 +1,81 @@
|
|
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
|
5
|
+
|
6
|
+
namespace :secure_headers do
|
7
|
+
include SecureHeaders::HashHelper
|
8
|
+
|
9
|
+
def is_erb?(filename)
|
10
|
+
filename =~ /\.erb\Z/
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_mustache?(filename)
|
14
|
+
filename =~ /\.mustache\Z/
|
15
|
+
end
|
16
|
+
|
17
|
+
def dynamic_content?(filename, inline_script)
|
18
|
+
(is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) ||
|
19
|
+
(is_erb?(filename) && inline_script =~ /<%.*%>/)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_inline_content(filename, regex, hashes)
|
23
|
+
file = File.read(filename)
|
24
|
+
file.scan(regex) do # TODO don't use gsub
|
25
|
+
inline_script = Regexp.last_match.captures.last
|
26
|
+
if dynamic_content?(filename, inline_script)
|
27
|
+
puts "Looks like there's some dynamic content inside of a tag :-/"
|
28
|
+
puts "That pretty much means the hash value will never match."
|
29
|
+
puts "Code: " + inline_script
|
30
|
+
puts "=" * 20
|
31
|
+
end
|
32
|
+
|
33
|
+
hashes << hash_source(inline_script)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_inline_script_hashes(filename)
|
38
|
+
hashes = []
|
39
|
+
|
40
|
+
[INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex|
|
41
|
+
find_inline_content(filename, regex, hashes)
|
42
|
+
end
|
43
|
+
|
44
|
+
hashes
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_inline_style_hashes(filename)
|
48
|
+
hashes = []
|
49
|
+
|
50
|
+
[INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex|
|
51
|
+
find_inline_content(filename, regex, hashes)
|
52
|
+
end
|
53
|
+
|
54
|
+
hashes
|
55
|
+
end
|
56
|
+
|
57
|
+
task :generate_hashes do |t, args|
|
58
|
+
script_hashes = {
|
59
|
+
"scripts" => {},
|
60
|
+
"styles" => {}
|
61
|
+
}
|
62
|
+
|
63
|
+
Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
|
64
|
+
hashes = generate_inline_script_hashes(filename)
|
65
|
+
if hashes.any?
|
66
|
+
script_hashes["scripts"][filename] = hashes
|
67
|
+
end
|
68
|
+
|
69
|
+
hashes = generate_inline_style_hashes(filename)
|
70
|
+
if hashes.any?
|
71
|
+
script_hashes["styles"][filename] = hashes
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
|
76
|
+
file.write(script_hashes.to_yaml)
|
77
|
+
end
|
78
|
+
|
79
|
+
puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
80
|
+
end
|
81
|
+
end
|
data/secure_headers.gemspec
CHANGED
@@ -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.
|
4
|
+
gem.version = "3.2.0"
|
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.'
|
@@ -36,7 +36,7 @@ module SecureHeaders
|
|
36
36
|
|
37
37
|
config = Configuration.get(:test_override)
|
38
38
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
39
|
-
[:csp, :dynamic_csp, :
|
39
|
+
[:csp, :dynamic_csp, :cookies].each do |key|
|
40
40
|
expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
|
41
41
|
end
|
42
42
|
end
|
@@ -82,5 +82,13 @@ module SecureHeaders
|
|
82
82
|
override_config = Configuration.get(:second_override)
|
83
83
|
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
84
84
|
end
|
85
|
+
|
86
|
+
it "deprecates the secure_cookies configuration" do
|
87
|
+
expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
|
88
|
+
|
89
|
+
Configuration.default do |config|
|
90
|
+
config.secure_cookies = true
|
91
|
+
end
|
92
|
+
end
|
85
93
|
end
|
86
94
|
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SecureHeaders
|
4
|
+
describe Cookie do
|
5
|
+
let(:raw_cookie) { "_session=thisisatest" }
|
6
|
+
|
7
|
+
it "does not tamper with cookies when unconfigured" do
|
8
|
+
cookie = Cookie.new(raw_cookie, {})
|
9
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "preserves existing attributes" do
|
13
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
14
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "prevents duplicate flagging of attributes" do
|
18
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
19
|
+
expect(cookie.to_s.scan(/secure/i).count).to eq(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
context "Secure cookies" do
|
23
|
+
context "when configured with a boolean" do
|
24
|
+
it "flags cookies as Secure" do
|
25
|
+
cookie = Cookie.new(raw_cookie, secure: true)
|
26
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when configured with a Hash" do
|
31
|
+
it "flags cookies as Secure when whitelisted" do
|
32
|
+
cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]})
|
33
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "does not flag cookies as Secure when excluded" do
|
37
|
+
cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] })
|
38
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "HttpOnly cookies" do
|
44
|
+
context "when configured with a boolean" do
|
45
|
+
it "flags cookies as HttpOnly" do
|
46
|
+
cookie = Cookie.new(raw_cookie, httponly: true)
|
47
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when configured with a Hash" do
|
52
|
+
it "flags cookies as HttpOnly when whitelisted" do
|
53
|
+
cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]})
|
54
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "does not flag cookies as HttpOnly when excluded" do
|
58
|
+
cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] })
|
59
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "SameSite cookies" do
|
65
|
+
it "flags SameSite=Lax" do
|
66
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
|
67
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "flags SameSite=Lax when configured with a boolean" do
|
71
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true})
|
72
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
73
|
+
end
|
74
|
+
|
75
|
+
it "does not flag cookies as SameSite=Lax when excluded" do
|
76
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
|
77
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "flags SameSite=Strict" do
|
81
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
|
82
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not flag cookies as SameSite=Strict when excluded" do
|
86
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
|
87
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "flags SameSite=Strict when configured with a boolean" do
|
91
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: true})
|
92
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "flags properly when both lax and strict are configured" do
|
96
|
+
raw_cookie = "_session=thisisatest"
|
97
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
|
98
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
99
|
+
end
|
100
|
+
|
101
|
+
it "ignores configuration if the cookie is already flagged" do
|
102
|
+
raw_cookie = "_session=thisisatest; SameSite=Strict"
|
103
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true })
|
104
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "with an invalid configuration" do
|
110
|
+
it "raises an exception when not configured with a Hash" do
|
111
|
+
expect do
|
112
|
+
Cookie.validate_config!("configuration")
|
113
|
+
end.to raise_error(CookiesConfigError)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "raises an exception when configured without a boolean/Hash" do
|
117
|
+
expect do
|
118
|
+
Cookie.validate_config!(secure: "true")
|
119
|
+
end.to raise_error(CookiesConfigError)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "raises an exception when both only and except filters are provided" do
|
123
|
+
expect do
|
124
|
+
Cookie.validate_config!(secure: { only: [], except: [] })
|
125
|
+
end.to raise_error(CookiesConfigError)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "raises an exception when SameSite is not configured with a Hash" do
|
129
|
+
expect do
|
130
|
+
Cookie.validate_config!(samesite: true)
|
131
|
+
end.to raise_error(CookiesConfigError)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
135
|
+
expect do
|
136
|
+
Cookie.validate_config!(samesite: { lax: true, strict: true})
|
137
|
+
end.to raise_error(CookiesConfigError)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
141
|
+
expect do
|
142
|
+
Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } })
|
143
|
+
end.to raise_error(CookiesConfigError)
|
144
|
+
end
|
145
|
+
|
146
|
+
it "raises an exception when both only and except filters are provided to SameSite configurations" do
|
147
|
+
expect do
|
148
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } })
|
149
|
+
end.to raise_error(CookiesConfigError)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
153
|
+
expect do
|
154
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } })
|
155
|
+
end.to raise_error(CookiesConfigError)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
159
|
+
expect do
|
160
|
+
Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } })
|
161
|
+
end.to raise_error(CookiesConfigError)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -10,9 +10,7 @@ module SecureHeaders
|
|
10
10
|
|
11
11
|
before(:each) do
|
12
12
|
reset_config
|
13
|
-
Configuration.default
|
14
|
-
# use all default provided by the library
|
15
|
-
end
|
13
|
+
Configuration.default
|
16
14
|
end
|
17
15
|
|
18
16
|
it "sets the headers" do
|
@@ -38,21 +36,56 @@ module SecureHeaders
|
|
38
36
|
expect(env[CSP::HEADER_NAME]).to match("example.org")
|
39
37
|
end
|
40
38
|
|
41
|
-
context "
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
context "secure_cookies" do
|
40
|
+
context "cookies should be flagged" do
|
41
|
+
it "flags cookies as secure" do
|
42
|
+
capture_warning do
|
43
|
+
Configuration.default { |config| config.secure_cookies = true }
|
44
|
+
end
|
45
|
+
request = Rack::Request.new("HTTPS" => "on")
|
46
|
+
_, env = cookie_middleware.call request.env
|
47
|
+
expect(env['Set-Cookie']).to eq("foo=bar; secure")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "cookies should not be flagged" do
|
52
|
+
it "does not flags cookies as secure" do
|
53
|
+
capture_warning do
|
54
|
+
Configuration.default { |config| config.secure_cookies = false }
|
55
|
+
end
|
56
|
+
request = Rack::Request.new("HTTPS" => "on")
|
57
|
+
_, env = cookie_middleware.call request.env
|
58
|
+
expect(env['Set-Cookie']).to eq("foo=bar")
|
59
|
+
end
|
47
60
|
end
|
48
61
|
end
|
49
62
|
|
50
|
-
context "cookies
|
51
|
-
it "
|
52
|
-
Configuration.default { |config| config.
|
53
|
-
request = Rack::
|
54
|
-
|
55
|
-
|
63
|
+
context "cookies" do
|
64
|
+
it "flags cookies from configuration" do
|
65
|
+
Configuration.default { |config| config.cookies = { secure: true, httponly: true } }
|
66
|
+
request = Rack::Request.new("HTTPS" => "on")
|
67
|
+
_, env = cookie_middleware.call request.env
|
68
|
+
|
69
|
+
expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "flags cookies with a combination of SameSite configurations" do
|
73
|
+
cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] })
|
74
|
+
|
75
|
+
Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } }
|
76
|
+
request = Rack::Request.new("HTTPS" => "on")
|
77
|
+
_, env = cookie_middleware.call request.env
|
78
|
+
|
79
|
+
expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict")
|
80
|
+
expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax")
|
81
|
+
end
|
82
|
+
|
83
|
+
it "disables secure cookies for non-https requests" do
|
84
|
+
Configuration.default { |config| config.cookies = { secure: true } }
|
85
|
+
|
86
|
+
request = Rack::Request.new("HTTPS" => "off")
|
87
|
+
_, env = cookie_middleware.call request.env
|
88
|
+
expect(env['Set-Cookie']).to eq("foo=bar")
|
56
89
|
end
|
57
90
|
end
|
58
91
|
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
class Message < ERB
|
5
|
+
include SecureHeaders::ViewHelpers
|
6
|
+
|
7
|
+
def self.template
|
8
|
+
<<-TEMPLATE
|
9
|
+
<% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %>
|
10
|
+
console.log(1)
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<% hashed_style_tag do %>
|
14
|
+
body {
|
15
|
+
background-color: black;
|
16
|
+
}
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<% nonced_javascript_tag do %>
|
20
|
+
body {
|
21
|
+
console.log(1)
|
22
|
+
}
|
23
|
+
<% end %>
|
24
|
+
|
25
|
+
<% nonced_style_tag do %>
|
26
|
+
body {
|
27
|
+
background-color: black;
|
28
|
+
}
|
29
|
+
<% end %>
|
30
|
+
<%= @name %>
|
31
|
+
|
32
|
+
TEMPLATE
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(request, options = {})
|
36
|
+
@virtual_path = "/asdfs/index"
|
37
|
+
@_request = request
|
38
|
+
@template = self.class.template
|
39
|
+
super(@template)
|
40
|
+
end
|
41
|
+
|
42
|
+
def capture(*args)
|
43
|
+
yield(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def content_tag(type, content = nil, options = nil, &block)
|
47
|
+
content = if block_given?
|
48
|
+
capture(block)
|
49
|
+
end
|
50
|
+
|
51
|
+
if options.is_a?(Hash)
|
52
|
+
options = options.map {|k,v| " #{k}=#{v}"}
|
53
|
+
end
|
54
|
+
"<#{type}#{options}>#{content}</#{type}>"
|
55
|
+
end
|
56
|
+
|
57
|
+
def result
|
58
|
+
super(binding)
|
59
|
+
end
|
60
|
+
|
61
|
+
def request
|
62
|
+
@_request
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module SecureHeaders
|
67
|
+
describe ViewHelpers do
|
68
|
+
let(:app) { lambda { |env| [200, env, "app"] } }
|
69
|
+
let(:middleware) { Middleware.new(app) }
|
70
|
+
let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) }
|
71
|
+
let(:filename) { "app/views/asdfs/index.html.erb" }
|
72
|
+
|
73
|
+
before(:all) do
|
74
|
+
Configuration.default do |config|
|
75
|
+
config.csp[:script_src] = %w('self')
|
76
|
+
config.csp[:style_src] = %w('self')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
after(:each) do
|
81
|
+
Configuration.instance_variable_set(:@script_hashes, nil)
|
82
|
+
Configuration.instance_variable_set(:@style_hashes, nil)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "raises an error when using hashed content without precomputed hashes" do
|
86
|
+
expect {
|
87
|
+
Message.new(request).result
|
88
|
+
}.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "raises an error when using hashed content with precomputed hashes, but none for the given file" do
|
92
|
+
Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"])
|
93
|
+
expect {
|
94
|
+
Message.new(request).result
|
95
|
+
}.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do
|
99
|
+
Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"])
|
100
|
+
expect {
|
101
|
+
Message.new(request).result
|
102
|
+
}.to raise_error(ViewHelpers::UnexpectedHashedScriptException)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "adds known hash values to the corresponding headers when the helper is used" do
|
106
|
+
begin
|
107
|
+
allow(SecureRandom).to receive(:base64).and_return("abc123")
|
108
|
+
|
109
|
+
expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8="
|
110
|
+
Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"])
|
111
|
+
expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc="
|
112
|
+
Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"])
|
113
|
+
|
114
|
+
# render erb that calls out to helpers.
|
115
|
+
Message.new(request).result
|
116
|
+
_, env = middleware.call request.env
|
117
|
+
|
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)}'/)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -45,3 +45,15 @@ end
|
|
45
45
|
def reset_config
|
46
46
|
SecureHeaders::Configuration.clear_configurations
|
47
47
|
end
|
48
|
+
|
49
|
+
def capture_warning
|
50
|
+
begin
|
51
|
+
old_stderr = $stderr
|
52
|
+
$stderr = StringIO.new
|
53
|
+
yield
|
54
|
+
result = $stderr.string
|
55
|
+
ensure
|
56
|
+
$stderr = old_stderr
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
data/upgrading-to-3-0.md
CHANGED
@@ -8,6 +8,7 @@ Changes
|
|
8
8
|
| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
|
9
9
|
| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
|
10
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`
|
11
12
|
| `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
|
12
13
|
| `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
|
13
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) |
|
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.
|
4
|
+
version: 3.2.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: 2016-
|
11
|
+
date: 2016-04-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -58,7 +58,9 @@ files:
|
|
58
58
|
- Rakefile
|
59
59
|
- lib/secure_headers.rb
|
60
60
|
- lib/secure_headers/configuration.rb
|
61
|
+
- lib/secure_headers/hash_helper.rb
|
61
62
|
- lib/secure_headers/headers/content_security_policy.rb
|
63
|
+
- lib/secure_headers/headers/cookie.rb
|
62
64
|
- lib/secure_headers/headers/policy_management.rb
|
63
65
|
- lib/secure_headers/headers/public_key_pins.rb
|
64
66
|
- lib/secure_headers/headers/strict_transport_security.rb
|
@@ -69,10 +71,13 @@ files:
|
|
69
71
|
- lib/secure_headers/headers/x_xss_protection.rb
|
70
72
|
- lib/secure_headers/middleware.rb
|
71
73
|
- lib/secure_headers/railtie.rb
|
74
|
+
- lib/secure_headers/utils/cookies_config.rb
|
72
75
|
- lib/secure_headers/view_helper.rb
|
76
|
+
- lib/tasks/tasks.rake
|
73
77
|
- secure_headers.gemspec
|
74
78
|
- spec/lib/secure_headers/configuration_spec.rb
|
75
79
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
80
|
+
- spec/lib/secure_headers/headers/cookie_spec.rb
|
76
81
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
77
82
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
78
83
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
@@ -82,6 +87,7 @@ files:
|
|
82
87
|
- spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb
|
83
88
|
- spec/lib/secure_headers/headers/x_xss_protection_spec.rb
|
84
89
|
- spec/lib/secure_headers/middleware_spec.rb
|
90
|
+
- spec/lib/secure_headers/view_helpers_spec.rb
|
85
91
|
- spec/lib/secure_headers_spec.rb
|
86
92
|
- spec/spec_helper.rb
|
87
93
|
- upgrading-to-3-0.md
|
@@ -113,6 +119,7 @@ summary: Add easily configured security headers to responses including content-s
|
|
113
119
|
test_files:
|
114
120
|
- spec/lib/secure_headers/configuration_spec.rb
|
115
121
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
122
|
+
- spec/lib/secure_headers/headers/cookie_spec.rb
|
116
123
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
117
124
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
118
125
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
@@ -122,5 +129,6 @@ test_files:
|
|
122
129
|
- spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb
|
123
130
|
- spec/lib/secure_headers/headers/x_xss_protection_spec.rb
|
124
131
|
- spec/lib/secure_headers/middleware_spec.rb
|
132
|
+
- spec/lib/secure_headers/view_helpers_spec.rb
|
125
133
|
- spec/lib/secure_headers_spec.rb
|
126
134
|
- spec/spec_helper.rb
|