secure_headers 3.1.2 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
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
|