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