Skip to content

Commit 7a23cb6

Browse files
jhawthornKyFaSt
andauthored
Make SecureSecurityPolicyConfig significantly faster (#506)
We have been seeing this gem a lot in profiles. Must of this slowness seems to come from overuse of instance variables in `DynamicConfig` and attempting to use them basically as a hash (which we can do much faster with a hash 😅) The first commit of these is the most important, but the other two also significantly speed things up. There is definitely more improvement available here, we seem to be overly cautious in duplicating arrays, and we also seem to convert unnecessarily between hashes and the config object, but I think this is the best place to start. <details> <summary>Benchmark:</summary> ``` require "secure_headers" require "benchmark/ips" # Copied from README MyCSPConfig = SecureHeaders::ContentSecurityPolicyConfig.new( preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. disable_nonce_backwards_compatibility: true, # default: false. If false, `unsafe-inline` will be added automatically when using nonces. If true, it won't. See #403 for why you'd want this. # directive values: these values will directly translate into source directives default_src: %w('none'), base_uri: %w('self'), block_all_mixed_content: true, # see https://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), form_action: %w('self' github.com), frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), sandbox: true, # true and [] will set a maximally restrictive setting plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), script_src_elem: %w('self'), script_src_attr: %w('self'), style_src: %w('unsafe-inline'), style_src_elem: %w('unsafe-inline'), style_src_attr: %w('unsafe-inline'), worker_src: %w('self'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) ) Benchmark.ips do |x| x.report "csp_config.to_h" do MyCSPConfig.to_h end x.report "csp_config.append" do MyCSPConfig.append({}) end x.report "new(config).value" do SecureHeaders::ContentSecurityPolicy.new(MyCSPConfig).value end end ``` </details> **Before:** ``` $ be ruby bench.rb Warming up -------------------------------------- csp_config.to_h 13.737k i/100ms csp_config.append 2.105k i/100ms new(config).value 1.429k i/100ms Calculating ------------------------------------- csp_config.to_h 139.988k (± 0.3%) i/s - 700.587k in 5.004666s csp_config.append 21.133k (± 2.4%) i/s - 107.355k in 5.082856s new(config).value 14.298k (± 0.4%) i/s - 72.879k in 5.097116s ``` **After:** ``` $ be ruby bench.rb Warming up -------------------------------------- csp_config.to_h 123.784k i/100ms csp_config.append 4.181k i/100ms new(config).value 1.617k i/100ms Calculating ------------------------------------- csp_config.to_h 1.238M (± 3.1%) i/s - 6.189M in 5.003769s csp_config.append 40.921k (± 1.0%) i/s - 204.869k in 5.006924s new(config).value 16.095k (± 0.4%) i/s - 80.850k in 5.023259s ``` `to_h` is 10x faster, `append` is 2x faster, and .value (which was not the target of these optimizations but I didn't want to see it regress) is slightly faster --------- Co-authored-by: Kylie Stradley <[email protected]>
1 parent ff9797f commit 7a23cb6

File tree

4 files changed

+26
-63
lines changed

4 files changed

+26
-63
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ source "https://rubygems.org"
33

44
gemspec
55

6+
gem "benchmark-ips"
7+
68
group :test do
79
gem "coveralls"
810
gem "json"

lib/secure_headers/configuration.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,17 @@ def named_append_or_override_exists?(name)
8383
# can lead to modifying parent objects.
8484
def deep_copy(config)
8585
return unless config
86-
config.each_with_object({}) do |(key, value), hash|
87-
hash[key] =
88-
if value.is_a?(Array)
86+
result = {}
87+
config.each_pair do |key, value|
88+
result[key] =
89+
case value
90+
when Array
8991
value.dup
9092
else
9193
value
9294
end
9395
end
96+
result
9497
end
9598

9699
# Private: Returns the internal default configuration. This should only

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def initialize(config = nil)
2020
config
2121
end
2222

23-
@preserve_schemes = @config.preserve_schemes
24-
@script_nonce = @config.script_nonce
25-
@style_nonce = @config.style_nonce
23+
@preserve_schemes = @config[:preserve_schemes]
24+
@script_nonce = @config[:script_nonce]
25+
@style_nonce = @config[:style_nonce]
2626
end
2727

2828
##

lib/secure_headers/headers/content_security_policy_config.rb

Lines changed: 15 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,23 @@
11
# frozen_string_literal: true
22
module SecureHeaders
33
module DynamicConfig
4-
def self.included(base)
5-
base.send(:attr_reader, *base.attrs)
6-
base.attrs.each do |attr|
7-
base.send(:define_method, "#{attr}=") do |value|
8-
if self.class.attrs.include?(attr)
9-
write_attribute(attr, value)
10-
else
11-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
12-
end
13-
end
14-
end
15-
end
16-
174
def initialize(hash)
18-
@base_uri = nil
19-
@child_src = nil
20-
@connect_src = nil
21-
@default_src = nil
22-
@font_src = nil
23-
@form_action = nil
24-
@frame_ancestors = nil
25-
@frame_src = nil
26-
@img_src = nil
27-
@manifest_src = nil
28-
@media_src = nil
29-
@navigate_to = nil
30-
@object_src = nil
31-
@plugin_types = nil
32-
@prefetch_src = nil
33-
@preserve_schemes = nil
34-
@report_only = nil
35-
@report_uri = nil
36-
@require_sri_for = nil
37-
@require_trusted_types_for = nil
38-
@sandbox = nil
39-
@script_nonce = nil
40-
@script_src = nil
41-
@script_src_elem = nil
42-
@script_src_attr = nil
43-
@style_nonce = nil
44-
@style_src = nil
45-
@style_src_elem = nil
46-
@style_src_attr = nil
47-
@trusted_types = nil
48-
@worker_src = nil
49-
@upgrade_insecure_requests = nil
50-
@disable_nonce_backwards_compatibility = nil
5+
@config = {}
516

527
from_hash(hash)
538
end
549

10+
def initialize_copy(hash)
11+
@config = hash.to_h
12+
end
13+
5514
def update_directive(directive, value)
56-
self.send("#{directive}=", value)
15+
@config[directive] = value
5716
end
5817

5918
def directive_value(directive)
60-
if self.class.attrs.include?(directive)
61-
self.send(directive)
62-
end
19+
# No need to check attrs, as we only assign valid keys
20+
@config[directive]
6321
end
6422

6523
def merge(new_hash)
@@ -77,10 +35,7 @@ def append(new_hash)
7735
end
7836

7937
def to_h
80-
self.class.attrs.each_with_object({}) do |key, hash|
81-
value = self.send(key)
82-
hash[key] = value unless value.nil?
83-
end
38+
@config.dup
8439
end
8540

8641
def dup
@@ -113,16 +68,19 @@ def from_hash(hash)
11368

11469
def write_attribute(attr, value)
11570
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
116-
attr_variable = "@#{attr}"
117-
self.instance_variable_set(attr_variable, value)
71+
if value.nil?
72+
@config.delete(attr)
73+
else
74+
@config[attr] = value
75+
end
11876
end
11977
end
12078

12179
class ContentSecurityPolicyConfigError < StandardError; end
12280
class ContentSecurityPolicyConfig
12381
HEADER_NAME = "Content-Security-Policy".freeze
12482

125-
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
83+
ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
12684
def self.attrs
12785
ATTRS
12886
end

0 commit comments

Comments
 (0)