Skip to content

Commit 44c8c3e

Browse files
committed
Merge pull request #208 from twitter/dont-bust-cache-for-idempotent-changes
If a policy modification is idempotent, don't dup the config
2 parents f23a66f + 3de87f9 commit 44c8c3e

File tree

3 files changed

+41
-17
lines changed

3 files changed

+41
-17
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: 14 additions & 9 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.to_s == combine_policies(config, additions).to_s
185+
end
186+
182187
# Public: combine the values from two different configs.
183188
#
184189
# original - the main config
@@ -208,11 +213,11 @@ def combine_policies(original, additions)
208213
# when each hash contains a value for a given key.
209214
original.merge(additions) do |directive, lhs, rhs|
210215
if source_list?(directive)
211-
lhs | rhs
216+
(lhs.to_a + rhs).uniq.compact
212217
else
213218
rhs
214219
end
215-
end
220+
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
216221
end
217222

218223
private

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ 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+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
121+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
122+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
123+
end
124+
112125
describe "#value" do
113126
it "discards 'none' values if any other source expressions are present" do
114127
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))

0 commit comments

Comments
 (0)