@@ -3,6 +3,7 @@ class Configuration
33 DEFAULT_CONFIG = :default
44 NOOP_CONFIGURATION = "secure_headers_noop_config"
55 class NotYetConfiguredError < StandardError ; end
6+ class IllegalPolicyModificationError < StandardError ; end
67 class << self
78 # Public: Set the global default configuration.
89 #
@@ -23,12 +24,12 @@ def default(&block)
2324 # if no value is supplied.
2425 #
2526 # Returns: the newly created config
26- def override ( name , base = DEFAULT_CONFIG )
27+ def override ( name , base = DEFAULT_CONFIG , & block )
2728 unless get ( base )
2829 raise NotYetConfiguredError , "#{ base } policy not yet supplied"
2930 end
3031 override = @configurations [ base ] . dup
31- yield ( override )
32+ override . instance_eval & block if block_given?
3233 add_configuration ( name , override )
3334 end
3435
@@ -43,18 +44,6 @@ def get(name = DEFAULT_CONFIG)
4344 @configurations [ name ]
4445 end
4546
46- # Public: perform a basic deep dup. The shallow copy provided by dup/clone
47- # can lead to modifying parent objects.
48- def deep_copy ( config )
49- config . each_with_object ( { } ) do |( key , value ) , hash |
50- hash [ key ] = if value . is_a? ( Array )
51- value . dup
52- else
53- value
54- end
55- end
56- end
57-
5847 private
5948
6049 # Private: add a valid configuration to the global set of named configs.
@@ -86,16 +75,39 @@ def add_noop_configuration
8675
8776 add_configuration ( NOOP_CONFIGURATION , noop_config )
8877 end
78+
79+ # Public: perform a basic deep dup. The shallow copy provided by dup/clone
80+ # can lead to modifying parent objects.
81+ def deep_copy ( config )
82+ config . each_with_object ( { } ) do |( key , value ) , hash |
83+ hash [ key ] = if value . is_a? ( Array )
84+ value . dup
85+ else
86+ value
87+ end
88+ end
89+ end
90+
91+ # Private: convenience method purely DRY things up. The value may not be a
92+ # hash (e.g. OPT_OUT, nil)
93+ def deep_copy_if_hash ( value )
94+ if value . is_a? ( Hash )
95+ deep_copy ( value )
96+ else
97+ value
98+ end
99+ end
89100 end
90101
91- attr_accessor :hsts , :x_frame_options , :x_content_type_options ,
92- :x_xss_protection , :csp , :x_download_options , :x_permitted_cross_domain_policies ,
93- :hpkp , :secure_cookies
94- attr_reader :cached_headers
102+ attr_writer :hsts , :x_frame_options , :x_content_type_options ,
103+ :x_xss_protection , :x_download_options , :x_permitted_cross_domain_policies ,
104+ :hpkp , :dynamic_csp , :secure_cookies
105+
106+ attr_reader :cached_headers , :csp , :dynamic_csp , :secure_cookies
95107
96108 def initialize ( &block )
97109 self . hpkp = OPT_OUT
98- self . csp = self . class . deep_copy ( CSP ::DEFAULT_CONFIG )
110+ self . csp = self . class . send ( :deep_copy , CSP ::DEFAULT_CONFIG )
99111 instance_eval &block if block_given?
100112 end
101113
@@ -104,33 +116,37 @@ def initialize(&block)
104116 # Returns a deep-dup'd copy of this configuration.
105117 def dup
106118 copy = self . class . new
107- copy . hsts = hsts
108- copy . x_frame_options = x_frame_options
109- copy . x_content_type_options = x_content_type_options
110- copy . x_xss_protection = x_xss_protection
111- copy . x_download_options = x_download_options
112- copy . x_permitted_cross_domain_policies = x_permitted_cross_domain_policies
113- copy . csp = if csp . is_a? ( Hash )
114- self . class . deep_copy ( csp )
115- else
116- csp
119+ copy . secure_cookies = @secure_cookies
120+ copy . csp = self . class . send ( :deep_copy_if_hash , @csp )
121+ copy . dynamic_csp = self . class . send ( :deep_copy_if_hash , @dynamic_csp )
122+ copy . cached_headers = self . class . send ( :deep_copy_if_hash , @cached_headers )
123+ copy
124+ end
125+
126+ def opt_out ( header )
127+ send ( "#{ header } =" , OPT_OUT )
128+ if header == CSP ::CONFIG_KEY
129+ dynamic_csp = OPT_OUT
117130 end
131+ self . cached_headers . delete ( header )
132+ end
133+
134+ def update_x_frame_options ( value )
135+ self . cached_headers [ XFrameOptions ::CONFIG_KEY ] = XFrameOptions . make_header ( value )
136+ end
118137
119- copy . hpkp = if hpkp . is_a? ( Hash )
120- self . class . deep_copy ( hpkp )
121- else
122- hpkp
138+ # Public: generated cached headers for a specific user agent.
139+ def rebuild_csp_header_cache! ( user_agent )
140+ self . cached_headers [ CSP ::CONFIG_KEY ] = { }
141+ unless current_csp == OPT_OUT
142+ user_agent = UserAgent . parse ( user_agent )
143+ variation = CSP . ua_to_variation ( user_agent )
144+ self . cached_headers [ CSP ::CONFIG_KEY ] [ variation ] = CSP . make_header ( current_csp , user_agent )
123145 end
124- copy
125146 end
126147
127- # Public: Retrieve a config based on the CONFIG_KEY for a class
128- #
129- # Returns the value if available, and returns a dup of any hash values.
130- def fetch ( key )
131- config = send ( key )
132- config = self . class . deep_copy ( config ) if config . is_a? ( Hash )
133- config
148+ def current_csp
149+ @dynamic_csp || @csp
134150 end
135151
136152 # Public: validates all configurations values.
@@ -139,24 +155,40 @@ def fetch(key)
139155 #
140156 # Returns nothing
141157 def validate_config!
142- StrictTransportSecurity . validate_config! ( hsts )
143- ContentSecurityPolicy . validate_config! ( csp )
144- XFrameOptions . validate_config! ( x_frame_options )
145- XContentTypeOptions . validate_config! ( x_content_type_options )
146- XXssProtection . validate_config! ( x_xss_protection )
147- XDownloadOptions . validate_config! ( x_download_options )
148- XPermittedCrossDomainPolicies . validate_config! ( x_permitted_cross_domain_policies )
149- PublicKeyPins . validate_config! ( hpkp )
158+ StrictTransportSecurity . validate_config! ( @ hsts)
159+ ContentSecurityPolicy . validate_config! ( @ csp)
160+ XFrameOptions . validate_config! ( @ x_frame_options)
161+ XContentTypeOptions . validate_config! ( @ x_content_type_options)
162+ XXssProtection . validate_config! ( @ x_xss_protection)
163+ XDownloadOptions . validate_config! ( @ x_download_options)
164+ XPermittedCrossDomainPolicies . validate_config! ( @ x_permitted_cross_domain_policies)
165+ PublicKeyPins . validate_config! ( @ hpkp)
150166 end
151167
168+ protected
169+
170+ def csp = ( new_csp )
171+ if self . dynamic_csp
172+ raise IllegalPolicyModificationError , "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
173+ end
174+
175+ @csp = new_csp
176+ end
177+
178+ def cached_headers = ( headers )
179+ @cached_headers = headers
180+ end
181+
182+ private
183+
152184 # Public: Precompute the header names and values for this configuraiton.
153185 # Ensures that headers generated at configure time, not on demand.
154186 #
155187 # Returns the cached headers
156188 def cache_headers!
157189 # generate defaults for the "easy" headers
158190 headers = ( ALL_HEADERS_BESIDES_CSP ) . each_with_object ( { } ) do |klass , hash |
159- config = fetch ( klass ::CONFIG_KEY )
191+ config = instance_variable_get ( "@ #{ klass ::CONFIG_KEY } " )
160192 unless config == OPT_OUT
161193 hash [ klass ::CONFIG_KEY ] = klass . make_header ( config ) . freeze
162194 end
@@ -165,7 +197,7 @@ def cache_headers!
165197 generate_csp_headers ( headers )
166198
167199 headers . freeze
168- @ cached_headers = headers
200+ self . cached_headers = headers
169201 end
170202
171203 # Private: adds CSP headers for each variation of CSP support.
@@ -175,11 +207,10 @@ def cache_headers!
175207 #
176208 # Returns nothing
177209 def generate_csp_headers ( headers )
178- unless csp == OPT_OUT
210+ unless @ csp == OPT_OUT
179211 headers [ CSP ::CONFIG_KEY ] = { }
180-
212+ csp_config = self . current_csp
181213 CSP ::VARIATIONS . each do |name , _ |
182- csp_config = fetch ( CSP ::CONFIG_KEY )
183214 csp = CSP . make_header ( csp_config , UserAgent . parse ( name ) )
184215 headers [ CSP ::CONFIG_KEY ] [ name ] = csp . freeze
185216 end
0 commit comments