Filename | /var/www/foswikidev/core/lib/Foswiki/Validation.pm |
Statements | Executed 14 statements in 1.26ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
1 | 1 | 1 | 13µs | 26µs | BEGIN@4 | Foswiki::Validation::
1 | 1 | 1 | 8µs | 13µs | BEGIN@5 | Foswiki::Validation::
1 | 1 | 1 | 8µs | 32µs | BEGIN@7 | Foswiki::Validation::
1 | 1 | 1 | 8µs | 38µs | BEGIN@61 | Foswiki::Validation::
1 | 1 | 1 | 5µs | 5µs | BEGIN@9 | Foswiki::Validation::
1 | 1 | 1 | 3µs | 3µs | BEGIN@12 | Foswiki::Validation::
1 | 1 | 1 | 3µs | 3µs | BEGIN@10 | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | _getSecret | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | _getSecretCookieName | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | addOnSubmit | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | addValidationKey | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | expireValidationKeys | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | generateValidationKey | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | getCookie | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | isValidNonce | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | isValidNonceHash | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | validate | Foswiki::Validation::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | # See bottom of file for license and copyright information | ||||
2 | package Foswiki::Validation; | ||||
3 | |||||
4 | 2 | 26µs | 2 | 38µs | # spent 26µs (13+12) within Foswiki::Validation::BEGIN@4 which was called:
# once (13µs+12µs) by Foswiki::UI::BEGIN@176 at line 4 # spent 26µs making 1 call to Foswiki::Validation::BEGIN@4
# spent 12µs making 1 call to strict::import |
5 | 2 | 22µs | 2 | 17µs | # spent 13µs (8+4) within Foswiki::Validation::BEGIN@5 which was called:
# once (8µs+4µs) by Foswiki::UI::BEGIN@176 at line 5 # spent 13µs making 1 call to Foswiki::Validation::BEGIN@5
# spent 4µs making 1 call to warnings::import |
6 | |||||
7 | 2 | 24µs | 2 | 55µs | # spent 32µs (8+23) within Foswiki::Validation::BEGIN@7 which was called:
# once (8µs+23µs) by Foswiki::UI::BEGIN@176 at line 7 # spent 32µs making 1 call to Foswiki::Validation::BEGIN@7
# spent 23µs making 1 call to Exporter::import |
8 | |||||
9 | 2 | 18µs | 1 | 5µs | # spent 5µs within Foswiki::Validation::BEGIN@9 which was called:
# once (5µs+0s) by Foswiki::UI::BEGIN@176 at line 9 # spent 5µs making 1 call to Foswiki::Validation::BEGIN@9 |
10 | 2 | 38µs | 1 | 3µs | # spent 3µs within Foswiki::Validation::BEGIN@10 which was called:
# once (3µs+0s) by Foswiki::UI::BEGIN@176 at line 10 # spent 3µs making 1 call to Foswiki::Validation::BEGIN@10 |
11 | |||||
12 | # spent 3µs within Foswiki::Validation::BEGIN@12 which was called:
# once (3µs+0s) by Foswiki::UI::BEGIN@176 at line 17 | ||||
13 | 1 | 7µs | if ( $Foswiki::cfg{UseLocale} ) { | ||
14 | require locale; | ||||
15 | import locale(); | ||||
16 | } | ||||
17 | 1 | 40µs | 1 | 3µs | } # spent 3µs making 1 call to Foswiki::Validation::BEGIN@12 |
18 | |||||
19 | =begin TML | ||||
20 | |||||
21 | ---+ package Foswiki::Validation | ||||
22 | |||||
23 | "Validation" is the process of ensuring that an incoming request came from | ||||
24 | a page we generated. Validation keys are injected into all HTML pages | ||||
25 | generated by Foswiki, in Foswiki::writeCompletePage. When a request is | ||||
26 | received from the browser that requires validation, that request must | ||||
27 | be accompanied by the validation key. The functions in this package | ||||
28 | support the generation and checking of these validation keys. | ||||
29 | |||||
30 | Two key validation methods are supported by this module; simple token | ||||
31 | validation, and double-submission validation. Simple token validation | ||||
32 | stores a magic number in the session, and then adds that magic number to | ||||
33 | all forms in the output HTML. When a form is submitted, the magic number | ||||
34 | submitted with the form must match the number stored in the session. This is | ||||
35 | a relatively weak protection method, but requires some coding around so may | ||||
36 | discourage many hackers. | ||||
37 | |||||
38 | The second method supported is properly called double cookie submission, | ||||
39 | but referred to as "strikeone" in Foswiki. This again uses a token added | ||||
40 | to output forms, but this time it uses Javascript to combine that token | ||||
41 | with a secret stored in a cookie, to create a new token. This is more secure | ||||
42 | because the cookie containing the secret cannot be read outside the domain | ||||
43 | of the server, making it much harder for a page hosted on an evil site to | ||||
44 | forge a valid transaction. | ||||
45 | |||||
46 | When a request requiring validation comes in, Foswiki::UI::checkValidationKey | ||||
47 | is called. This compares the key in the request with the set of valid keys | ||||
48 | stored in the session. If the comparison fails, the browser is redirected | ||||
49 | to the =login= script (even if the user is currently logged in) with the | ||||
50 | =action= parameter set to =validate=. This generates a confirmation screen | ||||
51 | that the user must accept before the transaction can proceed. When the screen | ||||
52 | is confirmed, =login= is invoked again and the original transaction restored | ||||
53 | from passthrough. | ||||
54 | |||||
55 | In the function descriptions below, $cgis is a reference to a CGI::Session | ||||
56 | object. | ||||
57 | |||||
58 | =cut | ||||
59 | |||||
60 | # Set to 1 to trace validation steps in STDERR | ||||
61 | 2 | 1.08ms | 2 | 68µs | # spent 38µs (8+30) within Foswiki::Validation::BEGIN@61 which was called:
# once (8µs+30µs) by Foswiki::UI::BEGIN@176 at line 61 # spent 38µs making 1 call to Foswiki::Validation::BEGIN@61
# spent 30µs making 1 call to constant::import |
62 | |||||
63 | # Define cookie name only once | ||||
64 | # WARNING: If you change this, be sure to also change the javascript | ||||
65 | sub _getSecretCookieName { 'FOSWIKISTRIKEONE' } | ||||
66 | |||||
67 | =begin TML | ||||
68 | |||||
69 | ---++ StaticMethod addValidationKey( $cgis, $context, $strikeone ) -> $form | ||||
70 | |||||
71 | Add a new validation key to a form. The key will time out after | ||||
72 | {Validation}{ValidForTime}. | ||||
73 | * =$cgis= - a CGI::Session | ||||
74 | * =$context= - the context for the key, usually the URL of the target | ||||
75 | page plus the time. This should be unique for each rendered page. | ||||
76 | * =$strikeone= - if set, expect the nonce to be combined with the | ||||
77 | session secret before it is posted back. | ||||
78 | The validation key will be added as a hidden parameter at the end of | ||||
79 | the form tag. | ||||
80 | |||||
81 | =cut | ||||
82 | |||||
83 | sub addValidationKey { | ||||
84 | my ( $cgis, $context, $strikeone ) = @_; | ||||
85 | |||||
86 | return '' unless ($cgis); | ||||
87 | |||||
88 | my $nonce = generateValidationKey( $cgis, $context, $strikeone ); | ||||
89 | |||||
90 | # Don't use CGI::hidden; it will inherit the URL param value of | ||||
91 | # validation key and override our value :-( | ||||
92 | return "<input type='hidden' name='validation_key' value='?$nonce' />"; | ||||
93 | } | ||||
94 | |||||
95 | =begin TML | ||||
96 | |||||
97 | ---++ StaticMethod generateValidationKey( $cgis, $context, $strikeone ) -> $nonce | ||||
98 | |||||
99 | Generate a new validation key. The key will time out after | ||||
100 | {Validation}{ValidForTime}. | ||||
101 | * =$cgis= - a CGI::Session | ||||
102 | * =$context= - the context for the key, usually the URL of the target | ||||
103 | page plus the time. This should be unique for each rendered page. | ||||
104 | * =$strikeone= - if set, expect the nonce to be combined with the | ||||
105 | session secret before it is posted back. | ||||
106 | The validation key can then be used in a HTML form, or headers for | ||||
107 | RestPlugin API etc. | ||||
108 | |||||
109 | =cut | ||||
110 | |||||
111 | # TODO: should this be callable from Foswiki::Func so that RestHandlers | ||||
112 | # can use it too? | ||||
113 | sub generateValidationKey { | ||||
114 | my ( $cgis, $context, $strikeone ) = @_; | ||||
115 | my $actions = $cgis->param('VALID_ACTIONS') || {}; | ||||
116 | |||||
117 | # Use scalar keys %$actions to ensure we generate a unique token | ||||
118 | # for each form on a page. | ||||
119 | my $nonce = Digest::MD5::md5_hex( | ||||
120 | Foswiki::encode_utf8( | ||||
121 | $context . $cgis->id() . scalar( keys %$actions ) | ||||
122 | ) | ||||
123 | ); | ||||
124 | my $action = $nonce; | ||||
125 | if ($strikeone) { | ||||
126 | |||||
127 | # When using strikeone, the validation key pushed into the form will | ||||
128 | # be combined with the secret in the cookie, and the combination | ||||
129 | # will be md5 encoded before sending back. Since we know the secret | ||||
130 | # and the validation key, then might as well save the hashed version. | ||||
131 | # This has to be consistent with the algorithm in strikeone.js | ||||
132 | my $secret = _getSecret($cgis); | ||||
133 | $action = Digest::MD5::md5_hex( $nonce, $secret ); | ||||
134 | |||||
135 | #print STDERR "V: STRIKEONE $nonce + $secret = $action\n" if TRACE; | ||||
136 | } | ||||
137 | my $timeout = time() + $Foswiki::cfg{Validation}{ValidForTime}; | ||||
138 | print STDERR "V: ADD KEY $action" | ||||
139 | . ( $nonce ne $action ? "($nonce)" : '' ) . ' = ' | ||||
140 | . $timeout . "\n" | ||||
141 | if TRACE && !defined $actions->{$action}; | ||||
142 | $actions->{$action} = $timeout; | ||||
143 | |||||
144 | #used to store the actions in case there are more than one form.. | ||||
145 | $cgis->param( 'VALID_ACTIONS', $actions ); | ||||
146 | |||||
147 | return $nonce; | ||||
148 | } | ||||
149 | |||||
150 | =begin TML | ||||
151 | |||||
152 | ---++ StaticMethod addOnSubmit( $form ) -> $form | ||||
153 | |||||
154 | Add a double submission onsubmit handler to a form. | ||||
155 | * =$form= - the opening tag of a form, ie. <form ...>= | ||||
156 | The handler will be added to an existing on submit, or by adding a new | ||||
157 | onsubmit in the form tag. | ||||
158 | |||||
159 | =cut | ||||
160 | |||||
161 | sub addOnSubmit { | ||||
162 | my ($form) = @_; | ||||
163 | unless ( $form =~ | ||||
164 | s/\bonsubmit=(["'])((?:\s*javascript:)?)(.*)\1/onsubmit=${1}${2}StrikeOne.submit(this);$3$1/i | ||||
165 | ) | ||||
166 | { | ||||
167 | $form =~ s/>$/ onsubmit="StrikeOne.submit(this)">/; | ||||
168 | } | ||||
169 | return $form; | ||||
170 | } | ||||
171 | |||||
172 | =begin TML | ||||
173 | |||||
174 | ---++ StaticMethod getCookie( $cgis ) -> $cookie | ||||
175 | |||||
176 | Get a double submission cookie | ||||
177 | * =$cgis= - a CGI::Session | ||||
178 | |||||
179 | The cookie is a non-HttpOnly cookie that contains the current session ID | ||||
180 | and a secret. The secret is constant for a given session. | ||||
181 | |||||
182 | =cut | ||||
183 | |||||
184 | sub getCookie { | ||||
185 | my ($cgis) = @_; | ||||
186 | |||||
187 | my $secret = _getSecret($cgis); | ||||
188 | |||||
189 | # Add the cookie to the response | ||||
190 | # TODO: -secure option should be abstraced out - see comments on Item:10061 | ||||
191 | require CGI::Cookie; | ||||
192 | my $cookie = CGI::Cookie->new( | ||||
193 | -name => _getSecretCookieName(), | ||||
194 | -value => $secret, | ||||
195 | -path => '/', | ||||
196 | -httponly => 0, # we *want* JS to be able to read it! | ||||
197 | ); | ||||
198 | |||||
199 | return $cookie; | ||||
200 | } | ||||
201 | |||||
202 | =begin TML | ||||
203 | |||||
204 | ---++ StaticMethod isValidNonce( $cgis, $key ) -> $boolean | ||||
205 | |||||
206 | Check that the given validation key is valid for the session. | ||||
207 | Return false if not. | ||||
208 | |||||
209 | =cut | ||||
210 | |||||
211 | sub isValidNonce { | ||||
212 | my ( $cgis, $nonce ) = @_; | ||||
213 | my $actions = $cgis->param('VALID_ACTIONS'); | ||||
214 | return isValidNonceHash( $actions, $nonce ); | ||||
215 | } | ||||
216 | |||||
217 | =begin TML | ||||
218 | |||||
219 | ---++ StaticMethod isValidNonceHash( $actions, $key ) -> $boolean | ||||
220 | |||||
221 | Check that the given validation key is valid for the session. | ||||
222 | Return false if not. | ||||
223 | |||||
224 | =cut | ||||
225 | |||||
226 | sub isValidNonceHash { | ||||
227 | my ( $actions, $nonce ) = @_; | ||||
228 | return 1 if ( $Foswiki::cfg{Validation}{Method} eq 'none' ); | ||||
229 | return 0 unless defined $nonce; | ||||
230 | $nonce =~ s/^\?// if ( $Foswiki::cfg{Validation}{Method} ne 'strikeone' ); | ||||
231 | return 0 unless ref($actions) eq 'HASH'; | ||||
232 | print STDERR "V: CHECK $nonce -> " . ( $actions->{$nonce} ? 1 : 0 ) . "\n" | ||||
233 | if TRACE; | ||||
234 | return $actions->{$nonce}; | ||||
235 | } | ||||
236 | |||||
237 | =begin TML | ||||
238 | |||||
239 | ---++ StaticMethod expireValidationKeys($cgis[, $key]) | ||||
240 | |||||
241 | Expire any timed-out validation keys for this session, and (optionally) | ||||
242 | force expiry of a specific key, even if it hasn't timed out. | ||||
243 | |||||
244 | =cut | ||||
245 | |||||
246 | sub expireValidationKeys { | ||||
247 | my ( $cgis, $key ) = @_; | ||||
248 | my $actions = $cgis->param('VALID_ACTIONS'); | ||||
249 | |||||
250 | if ($actions) { | ||||
251 | |||||
252 | if ( defined $key && exists $actions->{$key} ) { | ||||
253 | $actions->{$key} = 0; # force-expire this key | ||||
254 | } | ||||
255 | my $deaths = 0; | ||||
256 | my $now = time(); | ||||
257 | while ( my ( $nonce, $time ) = each %$actions ) { | ||||
258 | if ( $time < $now ) { | ||||
259 | |||||
260 | print STDERR "V: EXPIRE $nonce $time\n" if TRACE; | ||||
261 | delete $actions->{$nonce}; | ||||
262 | $deaths++; | ||||
263 | } | ||||
264 | } | ||||
265 | |||||
266 | # If we have more than the permitted number of keys, expire | ||||
267 | # the oldest ones. | ||||
268 | my $excess = | ||||
269 | scalar( keys %$actions ) - | ||||
270 | $Foswiki::cfg{Validation}{MaxKeysPerSession}; | ||||
271 | if ( $excess > 0 ) { | ||||
272 | print STDERR "V: $excess TOO MANY KEYS\n" if TRACE; | ||||
273 | my @keys = sort { $actions->{$a} <=> $actions->{$b} } | ||||
274 | keys %$actions; | ||||
275 | while ( $excess-- > 0 ) { | ||||
276 | my $key = shift(@keys); | ||||
277 | print STDERR "V: EXPIRE $key $actions->{$key}\n" if TRACE; | ||||
278 | delete $actions->{$key}; | ||||
279 | $deaths++; | ||||
280 | } | ||||
281 | } | ||||
282 | if ($deaths) { | ||||
283 | $cgis->param( 'VALID_ACTIONS', $actions ); | ||||
284 | } | ||||
285 | } | ||||
286 | } | ||||
287 | |||||
288 | =begin TML | ||||
289 | |||||
290 | ---++ StaticMethod validate($session) | ||||
291 | |||||
292 | Generate (or check) the "Suspicious request" verification screen for the | ||||
293 | given session. This screen is generated when a validation fails, as a | ||||
294 | response to a ValidationException. | ||||
295 | |||||
296 | =cut | ||||
297 | |||||
298 | sub validate { | ||||
299 | my ($session) = @_; | ||||
300 | my $query = $session->{request}; | ||||
301 | my $web = $session->{webName}; | ||||
302 | my $topic = $session->{topicName}; | ||||
303 | my $cgis = $session->getCGISession(); | ||||
304 | |||||
305 | my $tmpl = $session->templates->readTemplate('validate'); | ||||
306 | |||||
307 | if ( $query->param('response') ) { | ||||
308 | my $cacheUID = $query->param('foswikioriginalquery'); | ||||
309 | $query->delete('foswikioriginalquery'); | ||||
310 | my $url; | ||||
311 | if ( $query->param('response') eq 'OK' | ||||
312 | && isValidNonce( $cgis, scalar( $query->param('validation_key') ) ) | ||||
313 | ) | ||||
314 | { | ||||
315 | if ( !$cacheUID ) { | ||||
316 | $url = $session->getScriptUrl( 0, 'view', $web, $topic ); | ||||
317 | } | ||||
318 | else { | ||||
319 | |||||
320 | # Reload the cached original query over the current query. | ||||
321 | # When the redirect is validated it should pass, because | ||||
322 | # it will now be using the validation code from the | ||||
323 | # confirmation screen that brought us here. | ||||
324 | require Foswiki::Request::Cache; | ||||
325 | Foswiki::Request::Cache->new()->load( $cacheUID, $query ); | ||||
326 | $url = $query->url(); | ||||
327 | } | ||||
328 | |||||
329 | # Complete the query by passing the query on | ||||
330 | # with passthrough | ||||
331 | print STDERR "WV: CONFIRMED; POST to $url\n" if TRACE; | ||||
332 | $session->redirect( $url, 1 ); | ||||
333 | } | ||||
334 | else { | ||||
335 | print STDERR "V: CONFIRMATION REJECTED\n" if TRACE; | ||||
336 | |||||
337 | # Validation failed; redirect to view (302) | ||||
338 | $url = $session->getScriptUrl( 0, 'view', $web, $topic ); | ||||
339 | $session->redirect( $url, 0 ); # no passthrough | ||||
340 | } | ||||
341 | } | ||||
342 | else { | ||||
343 | |||||
344 | print STDERR "V: PROMPTING FOR CONFIRMATION " . $query->uri() . "\n" | ||||
345 | if TRACE; | ||||
346 | |||||
347 | # Prompt for user verification - code 419 chosen by foswiki devs. | ||||
348 | # None of the defined HTTP codes describe what is really happening, | ||||
349 | # which is why we chose a "new" code. The confirmation page | ||||
350 | # isn't a conflict, not a security issue, and we cannot use 403 | ||||
351 | # because there is a high probability this would get caught by | ||||
352 | # Apache to send back the Registation page. We didn't want any | ||||
353 | # installation to catch the HTTP return code we were sending back, | ||||
354 | # as we need this page to arrive intact to the user, otherwise | ||||
355 | # they won't be able to do anything. 419 is a placebo, and if it | ||||
356 | # is ever defined can be replaced by any other undefined 4xx code. | ||||
357 | $session->{response}->status(419); | ||||
358 | |||||
359 | my $topicObject = Foswiki::Meta->new( $session, $web, $topic ); | ||||
360 | $tmpl = $topicObject->expandMacros($tmpl); | ||||
361 | $tmpl = $topicObject->renderTML($tmpl); | ||||
362 | $tmpl =~ s/<nop>//g; | ||||
363 | |||||
364 | $session->writeCompletePage($tmpl); | ||||
365 | } | ||||
366 | } | ||||
367 | |||||
368 | # Get/set the one-strike secret in the CGI::Session | ||||
369 | sub _getSecret { | ||||
370 | my $cgis = shift; | ||||
371 | my $secret = $cgis->param( _getSecretCookieName() ); | ||||
372 | unless ($secret) { | ||||
373 | |||||
374 | # Use hex encoding to make it cookie-friendly | ||||
375 | $secret = Digest::MD5::md5_hex( $cgis->id(), rand(time) ); | ||||
376 | $cgis->param( _getSecretCookieName(), $secret ); | ||||
377 | } | ||||
378 | return $secret; | ||||
379 | } | ||||
380 | |||||
381 | 1 | 2µs | 1; | ||
382 | __END__ |