Skip to content

Commit 6ac2f46

Browse files
committed
Remove all notions of a set of stored configurations
The current code stored named configs in class instance hash. Since the only config that should ever exist is now the default config (all other cases are handled by dynamic overrides and not configs), we remove all vestiages of the configuration hash lookup.
1 parent 830b4b2 commit 6ac2f46

File tree

8 files changed

+77
-82
lines changed

8 files changed

+77
-82
lines changed

lib/secure_headers.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def opt_out_of_all_protection(request)
148148
def header_hash_for(request)
149149
prevent_dup = true
150150
config = config_for(request, prevent_dup)
151+
config.validate_config!
151152
user_agent = UserAgent.parse(request.user_agent)
152153
headers = config.generate_headers(user_agent)
153154

@@ -198,7 +199,7 @@ def content_security_policy_style_nonce(request)
198199
# Falls back to the global config
199200
def config_for(request, prevent_dup = false)
200201
config = request.env[SECURE_HEADERS_CONFIG] ||
201-
Configuration.get(Configuration::DEFAULT_CONFIG)
202+
Configuration.get
202203

203204

204205
# Global configs are frozen, per-request configs are not. When we're not

lib/secure_headers/configuration.rb

Lines changed: 46 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,25 @@ class << self
1414
#
1515
# Returns the newly created config.
1616
def default(&block)
17-
config = new(&block)
18-
add_configuration(DEFAULT_CONFIG, config)
17+
# Define a built-in override that clears all configuration options and
18+
# results in no security headers being set.
1919
override(NOOP_OVERRIDE) do |config|
2020
CONFIG_ATTRIBUTES.each do |attr|
2121
config.instance_variable_set("@#{attr}", OPT_OUT)
2222
end
2323
end
24-
config
24+
25+
new_config = new(&block).freeze
26+
new_config.validate_config!
27+
@default_config = new_config
2528
end
2629
alias_method :configure, :default
2730

31+
def get
32+
raise NotYetConfiguredError, "Default policy not yet supplied" unless @default_config
33+
@default_config
34+
end
35+
2836
# Public: create a named configuration that overrides the default config.
2937
#
3038
# name - use an idenfier for the override config.
@@ -34,6 +42,7 @@ def default(&block)
3442
# Returns: the newly created config
3543
def override(name, &block)
3644
@overrides ||= {}
45+
raise "Provide a configuration block" unless block_given?
3746
@overrides[name] = block
3847
end
3948

@@ -42,46 +51,19 @@ def overrides(name)
4251
@overrides[name]
4352
end
4453

45-
# Public: retrieve a global configuration object
46-
#
47-
# Returns the configuration with a given name or raises a
48-
# NotYetConfiguredError if `default` has not been called.
49-
def get(name = DEFAULT_CONFIG)
50-
if @configurations.nil?
51-
raise NotYetConfiguredError, "Default policy not yet supplied"
52-
end
53-
@configurations[name]
54-
end
55-
5654
def named_appends(name)
5755
@appends ||= {}
5856
@appends[name]
5957
end
6058

61-
def named_append(name, target = nil, &block)
59+
def named_append(name, &block)
6260
@appends ||= {}
6361
raise "Provide a configuration block" unless block_given?
6462
@appends[name] = block
6563
end
6664

6765
private
6866

69-
# Private: add a valid configuration to the global set of named configs.
70-
#
71-
# config - the config to store
72-
# name - the lookup value for this config
73-
#
74-
# Raises errors if the config is invalid or if a config named `name`
75-
# already exists.
76-
#
77-
# Returns the config, if valid
78-
def add_configuration(name, config)
79-
config.validate_config!
80-
@configurations ||= {}
81-
config.freeze
82-
@configurations[name] = config
83-
end
84-
8567
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
8668
# can lead to modifying parent objects.
8769
def deep_copy(config)
@@ -106,11 +88,7 @@ def deep_copy_if_hash(value)
10688
end
10789
end
10890

109-
NON_HEADER_ATTRIBUTES = [
110-
:cookies, :hpkp_report_host
111-
].freeze
112-
113-
HEADER_ATTRIBUTES_TO_HEADER_CLASSES = {
91+
CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
11492
hsts: StrictTransportSecurity,
11593
x_frame_options: XFrameOptions,
11694
x_content_type_options: XContentTypeOptions,
@@ -123,11 +101,18 @@ def deep_copy_if_hash(value)
123101
csp: ContentSecurityPolicy,
124102
csp_report_only: ContentSecurityPolicy,
125103
hpkp: PublicKeyPins,
104+
cookies: Cookie,
126105
}.freeze
127106

128-
CONFIG_ATTRIBUTES = (HEADER_ATTRIBUTES_TO_HEADER_CLASSES.keys + NON_HEADER_ATTRIBUTES).freeze
107+
CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze
129108

130-
attr_accessor(*CONFIG_ATTRIBUTES)
109+
# The list of attributes that must respond to a `validate_config!` method
110+
VALIDATABLE_ATTRIBUTES = CONFIG_ATTRIBUTES
111+
112+
# The list of attributes that must respond to a `make_header` method
113+
HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze
114+
115+
attr_accessor(*CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)
131116

132117
@script_hashes = nil
133118
@style_hashes = nil
@@ -144,7 +129,6 @@ def initialize(&block)
144129
@clear_site_data = nil
145130
@csp = nil
146131
@csp_report_only = nil
147-
@hpkp_report_host = nil
148132
@hpkp = nil
149133
@hsts = nil
150134
@x_content_type_options = nil
@@ -185,7 +169,8 @@ def dup
185169

186170
def generate_headers(user_agent)
187171
headers = {}
188-
HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass|
172+
HEADERABLE_ATTRIBUTES.each do |attr|
173+
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
189174
header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent)
190175
if header_name && value
191176
headers[header_name] = value
@@ -208,26 +193,26 @@ def update_x_frame_options(value)
208193
#
209194
# Returns nothing
210195
def validate_config!
211-
HEADER_ATTRIBUTES_TO_HEADER_CLASSES.each do |attr, klass|
196+
VALIDATABLE_ATTRIBUTES.each do |attr|
197+
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
212198
klass.validate_config!(instance_variable_get("@#{attr}"))
213199
end
214-
Cookie.validate_config!(@cookies)
215200
end
216201

217202
def secure_cookies=(secure_cookies)
218203
raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead."
219204
end
220205

221206
def csp=(new_csp)
222-
if new_csp.respond_to?(:opt_out?)
223-
@csp = new_csp.dup
207+
case new_csp
208+
when OPT_OUT
209+
@csp = new_csp
210+
when ContentSecurityPolicyConfig
211+
@csp = new_csp
212+
when Hash
213+
@csp = ContentSecurityPolicyConfig.new(new_csp)
224214
else
225-
if new_csp[:report_only]
226-
# invalid configuration implies that CSPRO should be set, CSP should not - so opt out
227-
raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
228-
else
229-
@csp = ContentSecurityPolicyConfig.new(new_csp)
230-
end
215+
raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
231216
end
232217
end
233218

@@ -238,34 +223,23 @@ def csp=(new_csp)
238223
# configuring csp_report_only, the code will assume you mean to only use
239224
# report-only mode and you will be opted-out of enforce mode.
240225
def csp_report_only=(new_csp)
241-
@csp_report_only = begin
242-
if new_csp.is_a?(ContentSecurityPolicyConfig)
243-
new_csp.make_report_only
244-
elsif new_csp.respond_to?(:opt_out?)
245-
new_csp.dup
246-
else
247-
if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
248-
raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
249-
else
250-
ContentSecurityPolicyReportOnlyConfig.new(new_csp)
251-
end
252-
end
226+
case new_csp
227+
when OPT_OUT
228+
@csp_report_only = new_csp
229+
when ContentSecurityPolicyReportOnlyConfig
230+
@csp_report_only = new_csp.dup
231+
when ContentSecurityPolicyConfig
232+
@csp_report_only = new_csp.make_report_only
233+
when Hash
234+
@csp_report_only = ContentSecurityPolicyReportOnlyConfig.new(new_csp)
235+
else
236+
raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
253237
end
254238
end
255239

256240
def hpkp_report_host
257241
return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri]
258242
URI.parse(@hpkp[:report_uri]).host
259243
end
260-
261-
protected
262-
263-
def cookies=(cookies)
264-
@cookies = cookies
265-
end
266-
267-
def hpkp=(hpkp)
268-
@hpkp = self.class.send(:deep_copy_if_hash, hpkp)
269-
end
270244
end
271245
end

lib/secure_headers/headers/content_security_policy_config.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def directive_value(directive)
5555

5656
def merge(new_hash)
5757
new_config = self.dup
58-
puts new_config.inspect
5958
new_config.send(:from_hash, new_hash)
6059
new_config
6160
end
@@ -141,7 +140,6 @@ def make_report_only
141140
end
142141

143142
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
144-
CONFIG_KEY = :csp_report_only
145143
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
146144

147145
def report_only?

lib/secure_headers/headers/policy_management.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,22 @@ def validate_config!(config)
216216
if config.directive_value(:script_src).nil?
217217
raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
218218
end
219+
if !config.report_only? && config.directive_value(:report_only)
220+
raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should sset :report_only to true")
221+
end
222+
223+
if config.report_only? && config.directive_value(:report_only) == false
224+
raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true")
225+
end
219226

220227
ContentSecurityPolicyConfig.attrs.each do |key|
221228
value = config.directive_value(key)
222229
next unless value
230+
223231
if META_CONFIGS.include?(key)
224232
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
233+
elsif NONCES.include?(key)
234+
raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil?
225235
else
226236
validate_directive!(key, value)
227237
end

spec/lib/secure_headers/configuration_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module SecureHeaders
99
end
1010

1111
it "has a default config" do
12-
expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil
12+
expect(Configuration.default).to_not be_nil
1313
end
1414

1515
it "has an 'noop' override" do

spec/lib/secure_headers/headers/policy_management_spec.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module SecureHeaders
1818
# (pulled from README)
1919
config = {
2020
# "meta" values. these will shape the header, but the values are not included in the header.
21-
report_only: true, # default: false
21+
report_only: false,
2222
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
2323

2424
# directive values: these values will directly translate into source directives
@@ -142,6 +142,18 @@ module SecureHeaders
142142
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
143143
end.to_not raise_error
144144
end
145+
146+
it "doesn't allow report_only to be set in a non-report-only config" do
147+
expect do
148+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true)))
149+
end.to raise_error(ContentSecurityPolicyConfigError)
150+
end
151+
152+
it "allows report_only to be set in a report-only config" do
153+
expect do
154+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true)))
155+
end.to_not raise_error
156+
end
145157
end
146158

147159
describe "#combine_policies" do

spec/lib/secure_headers_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ module SecureHeaders
131131
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
132132

133133
# append an unsupported directive
134-
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)})
134+
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)})
135135
# append a supported directive
136136
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
137137

@@ -337,7 +337,7 @@ module SecureHeaders
337337
report_only: true
338338
}
339339
end
340-
}.to raise_error(ArgumentError)
340+
}.to raise_error(ContentSecurityPolicyConfigError)
341341
end
342342

343343
it "Raises an error if csp_report_only is used with `report_only: false`" do

spec/spec_helper.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ def expect_default_values(hash)
4343
module SecureHeaders
4444
class Configuration
4545
class << self
46-
def clear_configurations
47-
@configurations = nil
46+
def clear_default_config
47+
@default_config = nil
4848
end
4949
end
5050
end
5151
end
5252

5353
def reset_config
54-
SecureHeaders::Configuration.clear_configurations
54+
SecureHeaders::Configuration.clear_default_config
5555
end

0 commit comments

Comments
 (0)