secure_headers 1.1.1 → 1.3.1
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.
- checksums.yaml +15 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +0 -4
- data/Guardfile +1 -5
- data/HISTORY.md +17 -0
- data/README.md +55 -26
- data/fixtures/rails_3_2_12/Gemfile +0 -2
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +10 -14
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -2
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +7 -12
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
- data/lib/secure_headers/headers/content_security_policy.rb +46 -36
- data/lib/secure_headers/padrino.rb +14 -0
- data/lib/secure_headers/version.rb +1 -1
- data/spec/controllers/content_security_policy_controller_spec.rb +19 -19
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +105 -85
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +20 -20
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +18 -18
- data/spec/lib/secure_headers_spec.rb +11 -11
- data/spec/spec_helper.rb +9 -24
- metadata +8 -12
- data/.rvmrc +0 -1
- data/fixtures/rails_3_2_12/Guardfile +0 -14
- data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'base64'
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
@@ -8,7 +9,7 @@ module SecureHeaders
|
|
|
8
9
|
STANDARD_HEADER_NAME = "Content-Security-Policy"
|
|
9
10
|
FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
|
|
10
11
|
DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
|
|
11
|
-
META = [:
|
|
12
|
+
META = [:disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
|
|
12
13
|
end
|
|
13
14
|
include Constants
|
|
14
15
|
|
|
@@ -30,6 +31,7 @@ module SecureHeaders
|
|
|
30
31
|
def initialize(config=nil, options={})
|
|
31
32
|
@experimental = !!options.delete(:experimental)
|
|
32
33
|
@controller = options.delete(:controller)
|
|
34
|
+
|
|
33
35
|
if options[:request]
|
|
34
36
|
parse_request(options[:request])
|
|
35
37
|
else
|
|
@@ -45,6 +47,10 @@ module SecureHeaders
|
|
|
45
47
|
configure(config) if config
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def nonce
|
|
51
|
+
@nonce ||= SecureRandom.base64(32).chomp
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
def configure(config)
|
|
49
55
|
@config = config.dup
|
|
50
56
|
|
|
@@ -54,21 +60,24 @@ module SecureHeaders
|
|
|
54
60
|
@config.merge!(experimental_config)
|
|
55
61
|
end
|
|
56
62
|
|
|
63
|
+
# http_additions will be the only field that still doesn't support
|
|
64
|
+
# lambdas because it's an ugly api that's showing it's age.
|
|
65
|
+
@http_additions = @config.delete(:http_additions)
|
|
66
|
+
|
|
67
|
+
normalize_csp_options
|
|
68
|
+
|
|
57
69
|
META.each do |meta|
|
|
58
70
|
self.send("#{meta}=", @config.delete(meta))
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
@
|
|
62
|
-
@script_nonce = @config.delete(:script_nonce)
|
|
63
|
-
|
|
64
|
-
normalize_csp_options
|
|
73
|
+
@enforce = @config.delete(:enforce)
|
|
65
74
|
normalize_reporting_endpoint
|
|
66
75
|
fill_directives unless disable_fill_missing?
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
def name
|
|
70
79
|
base = STANDARD_HEADER_NAME
|
|
71
|
-
if
|
|
80
|
+
if !@enforce || experimental
|
|
72
81
|
base += "-Report-Only"
|
|
73
82
|
end
|
|
74
83
|
base
|
|
@@ -89,16 +98,9 @@ module SecureHeaders
|
|
|
89
98
|
raise "Expected to find default_src directive value" unless @config[:default_src]
|
|
90
99
|
append_http_additions unless ssl_request?
|
|
91
100
|
header_value = [
|
|
92
|
-
# ensure default-src is first
|
|
93
|
-
build_directive(:default_src),
|
|
94
101
|
generic_directives(@config),
|
|
95
|
-
report_uri_directive
|
|
96
|
-
|
|
97
|
-
].join
|
|
98
|
-
|
|
99
|
-
#store the value for next time
|
|
100
|
-
@config = header_value
|
|
101
|
-
header_value.strip
|
|
102
|
+
report_uri_directive
|
|
103
|
+
].join.strip
|
|
102
104
|
rescue StandardError => e
|
|
103
105
|
raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
|
|
104
106
|
end
|
|
@@ -115,20 +117,31 @@ module SecureHeaders
|
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
def append_http_additions
|
|
118
|
-
return unless http_additions
|
|
119
|
-
http_additions.each do |k, v|
|
|
120
|
+
return unless @http_additions
|
|
121
|
+
@http_additions.each do |k, v|
|
|
120
122
|
@config[k] ||= []
|
|
121
123
|
@config[k] << v
|
|
122
124
|
end
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
def normalize_csp_options
|
|
126
|
-
@config.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
@config = @config.inject({}) do |hash, (key, value)|
|
|
129
|
+
# lambdas
|
|
130
|
+
config_val = value.respond_to?(:call) ? value.call : value
|
|
131
|
+
# space-delimeted strings
|
|
132
|
+
config_val = config_val.split if config_val.is_a? String
|
|
133
|
+
# array of strings
|
|
134
|
+
if config_val.respond_to?(:map) #skip booleans
|
|
135
|
+
config_val = config_val.map do |val|
|
|
136
|
+
translate_dir_value(val)
|
|
137
|
+
end.flatten.uniq
|
|
130
138
|
end
|
|
139
|
+
|
|
140
|
+
hash[key] = config_val
|
|
141
|
+
hash
|
|
131
142
|
end
|
|
143
|
+
|
|
144
|
+
@report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
|
|
132
145
|
end
|
|
133
146
|
|
|
134
147
|
# translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
|
|
@@ -138,6 +151,9 @@ module SecureHeaders
|
|
|
138
151
|
# self/none are special sources/src-dir-values and need to be quoted in chrome
|
|
139
152
|
elsif %{self none}.include?(val)
|
|
140
153
|
"'#{val}'"
|
|
154
|
+
elsif val == 'nonce'
|
|
155
|
+
@controller.instance_variable_set(:@content_security_policy_nonce, nonce)
|
|
156
|
+
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
141
157
|
else
|
|
142
158
|
val
|
|
143
159
|
end
|
|
@@ -161,8 +177,13 @@ module SecureHeaders
|
|
|
161
177
|
def same_origin?
|
|
162
178
|
return unless report_uri && request_uri
|
|
163
179
|
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
begin
|
|
181
|
+
origin = URI.parse(request_uri)
|
|
182
|
+
uri = URI.parse(report_uri)
|
|
183
|
+
rescue URI::InvalidURIError
|
|
184
|
+
return false
|
|
185
|
+
end
|
|
186
|
+
|
|
166
187
|
uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
|
|
167
188
|
end
|
|
168
189
|
|
|
@@ -180,26 +201,15 @@ module SecureHeaders
|
|
|
180
201
|
"report-uri #{@report_uri};"
|
|
181
202
|
end
|
|
182
203
|
|
|
183
|
-
def script_nonce_directive
|
|
184
|
-
return '' if @script_nonce.nil?
|
|
185
|
-
nonce_value = if @script_nonce.is_a?(String)
|
|
186
|
-
@script_nonce
|
|
187
|
-
elsif @controller
|
|
188
|
-
@controller.instance_exec(&@script_nonce)
|
|
189
|
-
else
|
|
190
|
-
@script_nonce.call
|
|
191
|
-
end
|
|
192
|
-
"script-nonce #{nonce_value};"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
204
|
def generic_directives(config)
|
|
196
205
|
header_value = ''
|
|
197
206
|
if config[:img_src]
|
|
198
207
|
config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
|
|
199
208
|
else
|
|
200
|
-
config[:img_src] = ['data:']
|
|
209
|
+
config[:img_src] = config[:default_src] + ['data:']
|
|
201
210
|
end
|
|
202
211
|
|
|
212
|
+
header_value = build_directive(:default_src)
|
|
203
213
|
config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
|
|
204
214
|
header_value += build_directive(k)
|
|
205
215
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module Padrino
|
|
3
|
+
class << self
|
|
4
|
+
##
|
|
5
|
+
# Main class that register this extension.
|
|
6
|
+
#
|
|
7
|
+
def registered(app)
|
|
8
|
+
app.extend SecureHeaders::ClassMethods
|
|
9
|
+
app.helpers SecureHeaders::InstanceMethods
|
|
10
|
+
end
|
|
11
|
+
alias :included :registered
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -32,58 +32,58 @@ describe ContentSecurityPolicyController do
|
|
|
32
32
|
let(:secondary_endpoint) { "https://internal.example.com" }
|
|
33
33
|
|
|
34
34
|
before(:each) do
|
|
35
|
-
SecureHeaders::Configuration.
|
|
36
|
-
subject.
|
|
37
|
-
subject.
|
|
38
|
-
subject.
|
|
39
|
-
Net::HTTP.
|
|
35
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
|
|
36
|
+
expect(subject).to receive :head
|
|
37
|
+
allow(subject).to receive(:params).and_return(params)
|
|
38
|
+
allow(subject).to receive(:request).and_return(FakeRequest.new)
|
|
39
|
+
allow_any_instance_of(Net::HTTP).to receive(:request)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
context "delivery endpoint" do
|
|
43
43
|
it "posts over ssl" do
|
|
44
|
-
subject.
|
|
44
|
+
expect(subject).to receive(:use_ssl)
|
|
45
45
|
subject.scribe
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
it "posts over plain http" do
|
|
49
|
-
SecureHeaders::Configuration.
|
|
50
|
-
subject.
|
|
49
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => 'http://example.com')
|
|
50
|
+
expect(subject).not_to receive(:use_ssl)
|
|
51
51
|
subject.scribe
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
it "makes a POST request" do
|
|
56
|
-
Net::HTTP.
|
|
57
|
-
request.
|
|
58
|
-
params.
|
|
56
|
+
allow(Net::HTTP).to receive(:new).and_return(request)
|
|
57
|
+
expect(request).to receive(:request).with(instance_of(::Net::HTTP::Post))
|
|
58
|
+
allow(params).to receive(:to_json)
|
|
59
59
|
subject.scribe
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
it "POSTs to the configured forward_endpoint" do
|
|
63
|
-
Net::HTTP::Post.
|
|
63
|
+
expect(Net::HTTP::Post).to receive(:new).with(secondary_endpoint).and_return(request)
|
|
64
64
|
subject.scribe
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
it "does not POST if there is no forwarder configured" do
|
|
68
|
-
SecureHeaders::Configuration.
|
|
69
|
-
Net::HTTP::Post.
|
|
68
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return({})
|
|
69
|
+
expect(Net::HTTP::Post).not_to receive(:new)
|
|
70
70
|
subject.scribe
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
it "eliminates known phony CSP reports" do
|
|
74
|
-
SecureHeaders::Configuration.
|
|
75
|
-
Net::HTTP::Post.
|
|
74
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => nil)
|
|
75
|
+
expect(Net::HTTP::Post).not_to receive :new
|
|
76
76
|
subject.scribe
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
it "logs errors when it cannot forward the CSP report" do
|
|
80
80
|
class Rails; def logger; end; end
|
|
81
81
|
logger = double(:repond_to? => true)
|
|
82
|
-
Rails.
|
|
82
|
+
allow(Rails).to receive(:logger).and_return(logger)
|
|
83
83
|
|
|
84
|
-
SecureHeaders::Configuration.
|
|
84
|
+
allow(SecureHeaders::Configuration).to receive(:csp).and_raise(StandardError)
|
|
85
85
|
|
|
86
|
-
logger.
|
|
86
|
+
expect(logger).to receive(:warn)
|
|
87
87
|
subject.scribe
|
|
88
88
|
end
|
|
89
89
|
end
|