Skip to content

Commit 645696f

Browse files
committed
If a policy modification is idempotent, don't dup the config
`dup`ing the config will bust the header value cache unnecessarily when a policy modification has no effect
1 parent af15d83 commit 645696f

File tree

3 files changed

+36
-15
lines changed

3 files changed

+36
-15
lines changed

lib/secure_headers.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,15 @@ class << self
4747
# additions - a hash containing directives. e.g.
4848
# script_src: %w(another-host.com)
4949
def override_content_security_policy_directives(request, additions)
50-
config = config_for(request).dup
51-
if config.csp == OPT_OUT
52-
config.csp = {}
50+
config = config_for(request)
51+
unless CSP.idempotent_additions?(config.csp, additions)
52+
config = config.dup
53+
if config.csp == OPT_OUT
54+
config.csp = {}
55+
end
56+
config.csp.merge!(additions)
57+
override_secure_headers_request_config(request, config)
5358
end
54-
config.csp.merge!(additions)
55-
override_secure_headers_request_config(request, config)
5659
end
5760

5861
# Public: appends source values to the current configuration. If no value
@@ -62,9 +65,12 @@ def override_content_security_policy_directives(request, additions)
6265
# additions - a hash containing directives. e.g.
6366
# script_src: %w(another-host.com)
6467
def append_content_security_policy_directives(request, additions)
65-
config = config_for(request).dup
66-
config.csp = CSP.combine_policies(config.csp, additions)
67-
override_secure_headers_request_config(request, config)
68+
config = config_for(request)
69+
unless CSP.idempotent_additions?(config.csp, additions)
70+
config = config.dup
71+
config.csp = CSP.combine_policies(config.csp, additions)
72+
override_secure_headers_request_config(request, config)
73+
end
6874
end
6975

7076
# Public: override X-Frame-Options settings for this request.

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,7 @@ def make_header(config, user_agent)
157157
[header.name, header.value]
158158
end
159159

160-
# Public: Validates that the configuration has a valid type, or that it is a valid
161-
# source expression.
162-
#
163-
# Private: validates that a source expression:
164-
# 1. has a valid name
165-
# 2. is an array of strings
166-
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
160+
# Public: Validates each source expression.
167161
#
168162
# Does not validate the invididual values of the source expression (e.g.
169163
# script_src => h*t*t*p: will not raise an exception)
@@ -179,6 +173,17 @@ def validate_config!(config)
179173
end
180174
end
181175

176+
# Public: determine if merging +additions+ will cause a change to the
177+
# actual value of the config.
178+
#
179+
# e.g. config = { script_src: %w(example.org google.com)} and
180+
# additions = { script_src: %w(google.com)} then idempotent_additions? would return
181+
# because google.com is already in the config.
182+
def idempotent_additions?(config, additions)
183+
return false if config == OPT_OUT
184+
config == combine_policies(config, additions)
185+
end
186+
182187
# Public: combine the values from two different configs.
183188
#
184189
# original - the main config

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ module SecureHeaders
109109
end
110110
end
111111

112+
describe "#idempotent_additions?" do
113+
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
114+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
115+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
116+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
117+
118+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
119+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
120+
end
121+
112122
describe "#value" do
113123
it "discards 'none' values if any other source expressions are present" do
114124
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))

0 commit comments

Comments
 (0)