@@ -3,6 +3,7 @@ class Configuration
3
3
DEFAULT_CONFIG = :default
4
4
NOOP_CONFIGURATION = "secure_headers_noop_config"
5
5
class NotYetConfiguredError < StandardError ; end
6
+ class IllegalPolicyModificationError < StandardError ; end
6
7
class << self
7
8
# Public: Set the global default configuration.
8
9
#
@@ -23,12 +24,12 @@ def default(&block)
23
24
# if no value is supplied.
24
25
#
25
26
# Returns: the newly created config
26
- def override ( name , base = DEFAULT_CONFIG )
27
+ def override ( name , base = DEFAULT_CONFIG , & block )
27
28
unless get ( base )
28
29
raise NotYetConfiguredError , "#{ base } policy not yet supplied"
29
30
end
30
31
override = @configurations [ base ] . dup
31
- yield ( override )
32
+ override . instance_eval & block if block_given?
32
33
add_configuration ( name , override )
33
34
end
34
35
@@ -43,18 +44,6 @@ def get(name = DEFAULT_CONFIG)
43
44
@configurations [ name ]
44
45
end
45
46
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
-
58
47
private
59
48
60
49
# Private: add a valid configuration to the global set of named configs.
@@ -86,16 +75,39 @@ def add_noop_configuration
86
75
87
76
add_configuration ( NOOP_CONFIGURATION , noop_config )
88
77
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
89
100
end
90
101
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
95
107
96
108
def initialize ( &block )
97
109
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 )
99
111
instance_eval &block if block_given?
100
112
end
101
113
@@ -104,33 +116,37 @@ def initialize(&block)
104
116
# Returns a deep-dup'd copy of this configuration.
105
117
def dup
106
118
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
117
130
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
118
137
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 )
123
145
end
124
- copy
125
146
end
126
147
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
134
150
end
135
151
136
152
# Public: validates all configurations values.
@@ -139,24 +155,40 @@ def fetch(key)
139
155
#
140
156
# Returns nothing
141
157
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)
150
166
end
151
167
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
+
152
184
# Public: Precompute the header names and values for this configuraiton.
153
185
# Ensures that headers generated at configure time, not on demand.
154
186
#
155
187
# Returns the cached headers
156
188
def cache_headers!
157
189
# generate defaults for the "easy" headers
158
190
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 } " )
160
192
unless config == OPT_OUT
161
193
hash [ klass ::CONFIG_KEY ] = klass . make_header ( config ) . freeze
162
194
end
@@ -165,7 +197,7 @@ def cache_headers!
165
197
generate_csp_headers ( headers )
166
198
167
199
headers . freeze
168
- @ cached_headers = headers
200
+ self . cached_headers = headers
169
201
end
170
202
171
203
# Private: adds CSP headers for each variation of CSP support.
@@ -175,11 +207,10 @@ def cache_headers!
175
207
#
176
208
# Returns nothing
177
209
def generate_csp_headers ( headers )
178
- unless csp == OPT_OUT
210
+ unless @ csp == OPT_OUT
179
211
headers [ CSP ::CONFIG_KEY ] = { }
180
-
212
+ csp_config = self . current_csp
181
213
CSP ::VARIATIONS . each do |name , _ |
182
- csp_config = fetch ( CSP ::CONFIG_KEY )
183
214
csp = CSP . make_header ( csp_config , UserAgent . parse ( name ) )
184
215
headers [ CSP ::CONFIG_KEY ] [ name ] = csp . freeze
185
216
end
0 commit comments