secure_headers 1.4.0 → 2.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.
- data/.gitignore +4 -8
- data/.travis.yml +7 -1
- data/Gemfile +4 -4
- data/README.md +115 -79
- data/Rakefile +11 -116
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -1
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +2 -1
- data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
- data/fixtures/rails_3_2_12/config.ru +3 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +55 -18
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +6 -1
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +5 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +5 -0
- data/fixtures/rails_4_1_8/Gemfile +5 -0
- data/fixtures/rails_4_1_8/README.rdoc +28 -0
- data/fixtures/rails_4_1_8/Rakefile +6 -0
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
- data/fixtures/rails_4_1_8/config/application.rb +15 -0
- data/fixtures/rails_4_1_8/config/boot.rb +4 -0
- data/fixtures/rails_4_1_8/config/environment.rb +5 -0
- data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
- data/fixtures/rails_4_1_8/config/routes.rb +4 -0
- data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
- data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
- data/fixtures/rails_4_1_8/config.ru +4 -0
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/hash_helper.rb +7 -0
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
- data/lib/secure_headers/headers/content_security_policy.rb +141 -135
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
- data/lib/secure_headers/railtie.rb +13 -22
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +59 -17
- data/lib/tasks/tasks.rake +48 -0
- data/secure_headers.gemspec +4 -4
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +1 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
- data/spec/lib/secure_headers_spec.rb +36 -61
- data/spec/spec_helper.rb +26 -1
- metadata +44 -11
- data/HISTORY.md +0 -162
- data/app/controllers/content_security_policy_controller.rb +0 -76
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
|
|
2
|
+
INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
3
|
+
SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
|
|
4
|
+
|
|
5
|
+
namespace :secure_headers do
|
|
6
|
+
include SecureHeaders::HashHelper
|
|
7
|
+
|
|
8
|
+
def is_erb?(filename)
|
|
9
|
+
filename =~ /\.erb\Z/
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate_inline_script_hashes(filename)
|
|
13
|
+
file = File.read(filename)
|
|
14
|
+
hashes = []
|
|
15
|
+
|
|
16
|
+
[INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex|
|
|
17
|
+
file.gsub(regex) do # TODO don't use gsub
|
|
18
|
+
inline_script = Regexp.last_match.captures.last
|
|
19
|
+
if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/)
|
|
20
|
+
puts "Looks like there's some dynamic content inside of a script tag :-/"
|
|
21
|
+
puts "That pretty much means the hash value will never match."
|
|
22
|
+
puts "Code: " + inline_script
|
|
23
|
+
puts "=" * 20
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
hashes << hash_source(inline_script)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
hashes
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
task :generate_hashes do |t, args|
|
|
34
|
+
script_hashes = {}
|
|
35
|
+
Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
|
|
36
|
+
hashes = generate_inline_script_hashes(filename)
|
|
37
|
+
if hashes.any?
|
|
38
|
+
script_hashes[filename] = hashes
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file|
|
|
43
|
+
file.write(script_hashes.to_yaml)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}"
|
|
47
|
+
end
|
|
48
|
+
end
|
data/secure_headers.gemspec
CHANGED
|
@@ -8,10 +8,10 @@ Gem::Specification.new do |gem|
|
|
|
8
8
|
gem.version = SecureHeaders::VERSION
|
|
9
9
|
gem.authors = ["Neil Matatall"]
|
|
10
10
|
gem.email = ["neil.matatall@gmail.com"]
|
|
11
|
-
gem.description = %q{
|
|
12
|
-
gem.summary = %q{Add easily configured
|
|
13
|
-
including content
|
|
14
|
-
strict-transport-security
|
|
11
|
+
gem.description = %q{Security related headers all in one gem.}
|
|
12
|
+
gem.summary = %q{Add easily configured security headers to responses
|
|
13
|
+
including content-security-policy, x-frame-options,
|
|
14
|
+
strict-transport-security, etc.}
|
|
15
15
|
gem.homepage = "https://github.com/twitter/secureheaders"
|
|
16
16
|
gem.license = "Apache Public License 2.0"
|
|
17
17
|
gem.files = `git ls-files`.split($/)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'secure_headers/headers/content_security_policy/script_hash_middleware'
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ContentSecurityPolicy::ScriptHashMiddleware do
|
|
6
|
+
|
|
7
|
+
let(:app) { double(:call => [200, headers, '']) }
|
|
8
|
+
let(:env) { double }
|
|
9
|
+
let(:headers) { double }
|
|
10
|
+
|
|
11
|
+
let(:default_config) do
|
|
12
|
+
{
|
|
13
|
+
:disable_fill_missing => true,
|
|
14
|
+
:default_src => 'https://*',
|
|
15
|
+
:report_uri => '/csp_report',
|
|
16
|
+
:script_src => 'inline eval https://* data:',
|
|
17
|
+
:style_src => "inline https://* about:"
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def should_assign_header name, value
|
|
22
|
+
expect(headers).to receive(:[]=).with(name, value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call_middleware(hashes = [])
|
|
26
|
+
options = {
|
|
27
|
+
:ua => USER_AGENTS[:chrome]
|
|
28
|
+
}
|
|
29
|
+
expect(env).to receive(:[]).with(HASHES_ENV_KEY).and_return(hashes)
|
|
30
|
+
expect(env).to receive(:[]).with(ENV_KEY).and_return(
|
|
31
|
+
:config => default_config,
|
|
32
|
+
:options => options
|
|
33
|
+
)
|
|
34
|
+
ContentSecurityPolicy::ScriptHashMiddleware.new(app).call(env)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "adds hashes stored in env to the header" do
|
|
38
|
+
should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*'sha256-/)
|
|
39
|
+
call_middleware(['sha256-abc123'])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "leaves things alone when no hashes are saved to env" do
|
|
43
|
+
should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*(?!'sha256-)/)
|
|
44
|
+
call_middleware()
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -4,12 +4,11 @@ module SecureHeaders
|
|
|
4
4
|
describe ContentSecurityPolicy do
|
|
5
5
|
let(:default_opts) do
|
|
6
6
|
{
|
|
7
|
-
:disable_chrome_extension => true,
|
|
8
7
|
:disable_fill_missing => true,
|
|
9
|
-
:default_src => 'https
|
|
8
|
+
:default_src => 'https:',
|
|
10
9
|
:report_uri => '/csp_report',
|
|
11
|
-
:script_src => 'inline eval https
|
|
12
|
-
:style_src => "inline https
|
|
10
|
+
:script_src => 'inline eval https: data:',
|
|
11
|
+
:style_src => "inline https: about:"
|
|
13
12
|
}
|
|
14
13
|
end
|
|
15
14
|
|
|
@@ -30,44 +29,41 @@ module SecureHeaders
|
|
|
30
29
|
|
|
31
30
|
describe "#name" do
|
|
32
31
|
context "when supplying options to override request" do
|
|
33
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(
|
|
34
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(
|
|
35
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(
|
|
36
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(
|
|
37
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(
|
|
32
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
33
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
34
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
35
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
36
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
context "when in report-only mode" do
|
|
41
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(
|
|
42
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(
|
|
43
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(
|
|
44
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(
|
|
45
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(
|
|
40
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
41
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
42
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
43
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
44
|
+
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
context "when in enforce mode" do
|
|
49
48
|
let(:opts) { default_opts.merge(:enforce => true)}
|
|
50
49
|
|
|
51
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(
|
|
52
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(
|
|
53
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(
|
|
54
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(
|
|
55
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(
|
|
50
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
|
|
51
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
|
|
52
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
|
|
53
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
|
|
54
|
+
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
|
|
56
55
|
end
|
|
56
|
+
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
63
|
-
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
64
|
-
specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
|
|
58
|
+
context "when using hash sources" do
|
|
59
|
+
it "adds hashes and unsafe-inline to the script-src" do
|
|
60
|
+
policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
|
|
61
|
+
expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
|
|
65
62
|
end
|
|
66
63
|
end
|
|
67
64
|
|
|
68
65
|
describe "#normalize_csp_options" do
|
|
69
66
|
before(:each) do
|
|
70
|
-
default_opts.delete(:disable_chrome_extension)
|
|
71
67
|
default_opts.delete(:disable_fill_missing)
|
|
72
68
|
default_opts[:script_src] << ' self none'
|
|
73
69
|
@opts = default_opts
|
|
@@ -76,11 +72,11 @@ module SecureHeaders
|
|
|
76
72
|
context "Content-Security-Policy" do
|
|
77
73
|
it "converts the script values to their equivilents" do
|
|
78
74
|
csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
|
|
79
|
-
expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https
|
|
75
|
+
expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
|
|
80
76
|
end
|
|
81
77
|
|
|
82
78
|
it "adds a @enforce and @app_name variables to the report uri" do
|
|
83
|
-
opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => 'twitter')
|
|
79
|
+
opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => lambda { 'twitter' })
|
|
84
80
|
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
85
81
|
expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
|
|
86
82
|
end
|
|
@@ -98,7 +94,7 @@ module SecureHeaders
|
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
csp = ContentSecurityPolicy.new(opts)
|
|
101
|
-
expect(csp.
|
|
97
|
+
expect(csp.value).to match("report-uri http://lambda/result")
|
|
102
98
|
end
|
|
103
99
|
|
|
104
100
|
it "accepts procs for other fields" do
|
|
@@ -115,130 +111,34 @@ module SecureHeaders
|
|
|
115
111
|
end
|
|
116
112
|
end
|
|
117
113
|
|
|
118
|
-
describe "#same_origin?" do
|
|
119
|
-
let(:origin) {"https://example.com:123"}
|
|
120
|
-
|
|
121
|
-
it "matches when host, scheme, and port match" do
|
|
122
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com"))
|
|
123
|
-
expect(csp.send(:same_origin?)).to be true
|
|
124
|
-
|
|
125
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com:443"))
|
|
126
|
-
expect(csp.send(:same_origin?)).to be true
|
|
127
|
-
|
|
128
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com:123'}, :request => request_for(FIREFOX, "https://example.com:123"))
|
|
129
|
-
expect(csp.send(:same_origin?)).to be true
|
|
130
|
-
|
|
131
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
132
|
-
expect(csp.send(:same_origin?)).to be true
|
|
133
|
-
|
|
134
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com:80'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
135
|
-
expect(csp.send(:same_origin?)).to be true
|
|
136
|
-
|
|
137
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:80"))
|
|
138
|
-
expect(csp.send(:same_origin?)).to be true
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
it "does not match port mismatches" do
|
|
142
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:81"))
|
|
143
|
-
expect(csp.send(:same_origin?)).to be false
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
it "does not match host mismatches" do
|
|
147
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://twitter.com'}, :request => request_for(FIREFOX, "http://example.com"))
|
|
148
|
-
expect(csp.send(:same_origin?)).to be false
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
it "does not match host mismatches because of subdomains" do
|
|
152
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://sub.example.com"))
|
|
153
|
-
expect(csp.send(:same_origin?)).to be false
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
it "does not match scheme mismatches" do
|
|
157
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "ftp://example.com"))
|
|
158
|
-
expect(csp.send(:same_origin?)).to be false
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
it "does not match on substring collisions" do
|
|
162
|
-
csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://anotherexample.com"))
|
|
163
|
-
expect(csp.send(:same_origin?)).to be false
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
describe "#normalize_reporting_endpoint" do
|
|
168
|
-
let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
|
|
169
|
-
|
|
170
|
-
context "when using firefox" do
|
|
171
|
-
it "updates the report-uri when posting to a different host" do
|
|
172
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://anexample.com"))
|
|
173
|
-
expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
it "doesn't change report-uri if a path supplied" do
|
|
177
|
-
csp = ContentSecurityPolicy.new({:report_uri => "/csp_reports"}, :request => request_for(FIREFOX, "https://anexample.com"))
|
|
178
|
-
expect(csp.report_uri).to eq("/csp_reports")
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
it "forwards if the request_uri is set to a non-matching value" do
|
|
182
|
-
csp = ContentSecurityPolicy.new({:report_uri => "https://another.example.com", :forward_endpoint => '/somewhere'}, :ua => "Firefox", :request_uri => "https://anexample.com")
|
|
183
|
-
expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
it "does not update the URI is the report_uri is on the same origin" do
|
|
188
|
-
opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
|
|
189
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://example.com/somewhere"))
|
|
190
|
-
expect(csp.report_uri).to eq('https://example.com/csp')
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
it "does not update the report-uri when using a non-firefox browser" do
|
|
194
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
|
195
|
-
expect(csp.report_uri).to eq('https://example.com/csp')
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
context "when using a protocol-relative value for report-uri" do
|
|
199
|
-
let(:opts) {
|
|
200
|
-
{
|
|
201
|
-
:default_src => 'self',
|
|
202
|
-
:report_uri => '//example.com/csp'
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
it "uses the current protocol" do
|
|
207
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
208
|
-
expect(csp.value).to match(%r{report-uri https://example.com/csp;})
|
|
209
|
-
|
|
210
|
-
csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX))
|
|
211
|
-
expect(csp.value).to match(%r{report-uri http://example.com/csp;})
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
it "uses the pre-configured https protocol" do
|
|
215
|
-
csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => true)
|
|
216
|
-
expect(csp.value).to match(%r{report-uri https://example.com/csp;})
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
it "uses the pre-configured http protocol" do
|
|
220
|
-
csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => false)
|
|
221
|
-
expect(csp.value).to match(%r{report-uri http://example.com/csp;})
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
114
|
describe "#value" do
|
|
227
115
|
it "raises an exception when default-src is missing" do
|
|
228
116
|
csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
|
|
229
117
|
expect {
|
|
230
118
|
csp.value
|
|
231
|
-
}.to raise_error(
|
|
119
|
+
}.to raise_error(RuntimeError)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context "CSP level 2 directives" do
|
|
123
|
+
let(:config) { {:default_src => 'self'} }
|
|
124
|
+
::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
|
|
125
|
+
it "supports all level 2 directives" do
|
|
126
|
+
directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
|
|
127
|
+
config.merge!({ non_default_source => "value" })
|
|
128
|
+
csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
|
|
129
|
+
expect(csp.value).to match(/#{directive_name} value;/)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
232
132
|
end
|
|
233
133
|
|
|
234
134
|
context "auto-whitelists data: uris for img-src" do
|
|
235
135
|
it "sets the value if no img-src specified" do
|
|
236
|
-
csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true
|
|
136
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
237
137
|
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
238
138
|
end
|
|
239
139
|
|
|
240
140
|
it "appends the value if img-src is specified" do
|
|
241
|
-
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true
|
|
141
|
+
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
242
142
|
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
243
143
|
end
|
|
244
144
|
end
|
|
@@ -246,7 +146,7 @@ module SecureHeaders
|
|
|
246
146
|
it "fills in directives without values with default-src value" do
|
|
247
147
|
options = default_opts.merge(:disable_fill_missing => false)
|
|
248
148
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
249
|
-
value = "default-src https
|
|
149
|
+
value = "default-src https:; connect-src https:; font-src https:; frame-src https:; img-src https: data:; media-src https:; object-src https:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;"
|
|
250
150
|
expect(csp.value).to eq(value)
|
|
251
151
|
end
|
|
252
152
|
|
|
@@ -258,19 +158,14 @@ module SecureHeaders
|
|
|
258
158
|
context "Firefox" do
|
|
259
159
|
it "builds a csp header for firefox" do
|
|
260
160
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
|
|
261
|
-
expect(csp.value).to eq("default-src https
|
|
161
|
+
expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
|
262
162
|
end
|
|
263
163
|
end
|
|
264
164
|
|
|
265
165
|
context "Chrome" do
|
|
266
166
|
it "builds a csp header for chrome" do
|
|
267
167
|
csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
|
|
268
|
-
expect(csp.value).to eq("default-src https
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
it "ignores :forward_endpoint settings" do
|
|
272
|
-
csp = ContentSecurityPolicy.new(@options_with_forwarding, :request => request_for(CHROME))
|
|
273
|
-
expect(csp.value).to match(/report-uri #{@options_with_forwarding[:report_uri]};/)
|
|
168
|
+
expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
|
274
169
|
end
|
|
275
170
|
end
|
|
276
171
|
|
|
@@ -299,41 +194,19 @@ module SecureHeaders
|
|
|
299
194
|
end
|
|
300
195
|
end
|
|
301
196
|
|
|
302
|
-
context "when supplying a experimental values" do
|
|
303
|
-
let(:options) {{
|
|
304
|
-
:disable_chrome_extension => true,
|
|
305
|
-
:disable_fill_missing => true,
|
|
306
|
-
:default_src => 'self',
|
|
307
|
-
:script_src => 'https://*',
|
|
308
|
-
:experimental => {
|
|
309
|
-
:script_src => 'self'
|
|
310
|
-
}
|
|
311
|
-
}}
|
|
312
|
-
|
|
313
|
-
it "returns the original value" do
|
|
314
|
-
header = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
315
|
-
expect(header.value).to eq("default-src 'self'; img-src 'self' data:; script-src https://*;")
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
it "it returns the experimental value if requested" do
|
|
319
|
-
header = ContentSecurityPolicy.new(options, {:request => request_for(CHROME), :experimental => true})
|
|
320
|
-
expect(header.value).not_to match(/https/)
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
|
|
324
197
|
context "when supplying additional http directive values" do
|
|
325
198
|
let(:options) {
|
|
326
199
|
default_opts.merge({
|
|
327
200
|
:http_additions => {
|
|
328
|
-
:frame_src => "http
|
|
329
|
-
:img_src => "http
|
|
201
|
+
:frame_src => "http:",
|
|
202
|
+
:img_src => "http:"
|
|
330
203
|
}
|
|
331
204
|
})
|
|
332
205
|
}
|
|
333
206
|
|
|
334
207
|
it "adds directive values for headers on http" do
|
|
335
208
|
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
|
336
|
-
expect(csp.value).to eq("default-src https
|
|
209
|
+
expect(csp.value).to eq("default-src https:; frame-src http:; img-src http: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
|
337
210
|
end
|
|
338
211
|
|
|
339
212
|
it "does not add the directive values if requesting https" do
|
|
@@ -345,45 +218,47 @@ module SecureHeaders
|
|
|
345
218
|
csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
|
|
346
219
|
expect(csp.value).not_to match(/http:/)
|
|
347
220
|
end
|
|
221
|
+
end
|
|
348
222
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let(:options) {{
|
|
355
|
-
:disable_chrome_extension => true,
|
|
356
|
-
:disable_fill_missing => true,
|
|
357
|
-
:default_src => 'self',
|
|
358
|
-
:script_src => 'https://*',
|
|
359
|
-
:http_additions => {
|
|
360
|
-
:script_src => 'http://*'
|
|
361
|
-
},
|
|
362
|
-
:experimental => {
|
|
363
|
-
:script_src => 'self',
|
|
364
|
-
:http_additions => {
|
|
365
|
-
:script_src => 'https://mycdn.example.com'
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}}
|
|
369
|
-
# for comparison purposes, if not using the experimental header this would produce
|
|
370
|
-
# "allow 'self'; script-src https://*" for https requests
|
|
371
|
-
# and
|
|
372
|
-
# "allow 'self'; script-src https://* http://*" for http requests
|
|
373
|
-
|
|
374
|
-
it "uses the value in the experimental block over SSL" do
|
|
375
|
-
csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX, '/', :ssl => true))
|
|
376
|
-
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:; script-src 'self';")
|
|
223
|
+
describe "class methods" do
|
|
224
|
+
let(:ua) { CHROME }
|
|
225
|
+
let(:env) do
|
|
226
|
+
double.tap do |env|
|
|
227
|
+
allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
|
|
377
228
|
end
|
|
229
|
+
end
|
|
230
|
+
let(:request) do
|
|
231
|
+
double(
|
|
232
|
+
:ssl? => true,
|
|
233
|
+
:url => 'https://example.com',
|
|
234
|
+
:env => env
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
describe ".add_to_env" do
|
|
239
|
+
let(:controller) { double }
|
|
240
|
+
let(:config) { {:default_src => 'self'} }
|
|
241
|
+
let(:options) { {:controller => controller} }
|
|
378
242
|
|
|
379
|
-
it "
|
|
380
|
-
|
|
381
|
-
|
|
243
|
+
it "adds metadata to env" do
|
|
244
|
+
metadata = {
|
|
245
|
+
:config => config,
|
|
246
|
+
:options => options
|
|
247
|
+
}
|
|
248
|
+
expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
|
|
249
|
+
expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
|
|
250
|
+
ContentSecurityPolicy.add_to_env(request, controller, config)
|
|
382
251
|
end
|
|
252
|
+
end
|
|
383
253
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
254
|
+
describe ".options_from_request" do
|
|
255
|
+
it "extracts options from request" do
|
|
256
|
+
options = ContentSecurityPolicy.options_from_request(request)
|
|
257
|
+
expect(options).to eql({
|
|
258
|
+
:ua => ua,
|
|
259
|
+
:ssl => true,
|
|
260
|
+
:request_uri => 'https://example.com'
|
|
261
|
+
})
|
|
387
262
|
end
|
|
388
263
|
end
|
|
389
264
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
describe XPermittedCrossDomainPolicies do
|
|
3
|
+
specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)}
|
|
4
|
+
specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")}
|
|
5
|
+
specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')}
|
|
6
|
+
specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') }
|
|
7
|
+
|
|
8
|
+
context "valid configuration values" do
|
|
9
|
+
it "accepts 'all'" do
|
|
10
|
+
expect {
|
|
11
|
+
XPermittedCrossDomainPolicies.new("all")
|
|
12
|
+
}.not_to raise_error
|
|
13
|
+
|
|
14
|
+
expect {
|
|
15
|
+
XPermittedCrossDomainPolicies.new(:value => "all")
|
|
16
|
+
}.not_to raise_error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "accepts 'by-ftp-filename'" do
|
|
20
|
+
expect {
|
|
21
|
+
XPermittedCrossDomainPolicies.new("by-ftp-filename")
|
|
22
|
+
}.not_to raise_error
|
|
23
|
+
|
|
24
|
+
expect {
|
|
25
|
+
XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename")
|
|
26
|
+
}.not_to raise_error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "accepts 'by-content-type'" do
|
|
30
|
+
expect {
|
|
31
|
+
XPermittedCrossDomainPolicies.new("by-content-type")
|
|
32
|
+
}.not_to raise_error
|
|
33
|
+
|
|
34
|
+
expect {
|
|
35
|
+
XPermittedCrossDomainPolicies.new(:value => "by-content-type")
|
|
36
|
+
}.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
it "accepts 'master-only'" do
|
|
39
|
+
expect {
|
|
40
|
+
XPermittedCrossDomainPolicies.new("master-only")
|
|
41
|
+
}.not_to raise_error
|
|
42
|
+
|
|
43
|
+
expect {
|
|
44
|
+
XPermittedCrossDomainPolicies.new(:value => "master-only")
|
|
45
|
+
}.not_to raise_error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "accepts nil" do
|
|
49
|
+
expect {
|
|
50
|
+
XPermittedCrossDomainPolicies.new
|
|
51
|
+
}.not_to raise_error
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'invlaid configuration values' do
|
|
56
|
+
|
|
57
|
+
it "doesn't accept invalid values" do
|
|
58
|
+
expect {
|
|
59
|
+
XPermittedCrossDomainPolicies.new("open")
|
|
60
|
+
}.to raise_error
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|