← Index
NYTProf Performance Profile   « line view »
For ./view
  Run on Fri Jul 31 18:42:36 2015
Reported on Fri Jul 31 18:48:14 2015

Filename/var/www/foswikidev/core/lib/Foswiki/Validation.pm
StatementsExecuted 14 statements in 1.26ms
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
11113µs26µsFoswiki::Validation::::BEGIN@4Foswiki::Validation::BEGIN@4
1118µs13µsFoswiki::Validation::::BEGIN@5Foswiki::Validation::BEGIN@5
1118µs32µsFoswiki::Validation::::BEGIN@7Foswiki::Validation::BEGIN@7
1118µs38µsFoswiki::Validation::::BEGIN@61Foswiki::Validation::BEGIN@61
1115µs5µsFoswiki::Validation::::BEGIN@9Foswiki::Validation::BEGIN@9
1113µs3µsFoswiki::Validation::::BEGIN@12Foswiki::Validation::BEGIN@12
1113µs3µsFoswiki::Validation::::BEGIN@10Foswiki::Validation::BEGIN@10
0000s0sFoswiki::Validation::::_getSecretFoswiki::Validation::_getSecret
0000s0sFoswiki::Validation::::_getSecretCookieNameFoswiki::Validation::_getSecretCookieName
0000s0sFoswiki::Validation::::addOnSubmitFoswiki::Validation::addOnSubmit
0000s0sFoswiki::Validation::::addValidationKeyFoswiki::Validation::addValidationKey
0000s0sFoswiki::Validation::::expireValidationKeysFoswiki::Validation::expireValidationKeys
0000s0sFoswiki::Validation::::generateValidationKeyFoswiki::Validation::generateValidationKey
0000s0sFoswiki::Validation::::getCookieFoswiki::Validation::getCookie
0000s0sFoswiki::Validation::::isValidNonceFoswiki::Validation::isValidNonce
0000s0sFoswiki::Validation::::isValidNonceHashFoswiki::Validation::isValidNonceHash
0000s0sFoswiki::Validation::::validateFoswiki::Validation::validate
Call graph for these subroutines as a Graphviz dot language file.
Line State
ments
Time
on line
Calls Time
in subs
Code
1# See bottom of file for license and copyright information
2package Foswiki::Validation;
3
4226µs238µ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
use strict;
# spent 26µs making 1 call to Foswiki::Validation::BEGIN@4 # spent 12µs making 1 call to strict::import
5222µs217µ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
use warnings;
# spent 13µs making 1 call to Foswiki::Validation::BEGIN@5 # spent 4µs making 1 call to warnings::import
6
7224µs255µ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
use Assert;
# spent 32µs making 1 call to Foswiki::Validation::BEGIN@7 # spent 23µs making 1 call to Exporter::import
8
9218µs15µs
# spent 5µs within Foswiki::Validation::BEGIN@9 which was called: # once (5µs+0s) by Foswiki::UI::BEGIN@176 at line 9
use Digest::MD5 ();
# spent 5µs making 1 call to Foswiki::Validation::BEGIN@9
10238µs13µs
# spent 3µs within Foswiki::Validation::BEGIN@10 which was called: # once (3µs+0s) by Foswiki::UI::BEGIN@176 at line 10
use Foswiki ();
# 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
BEGIN {
1317µs if ( $Foswiki::cfg{UseLocale} ) {
14 require locale;
15 import locale();
16 }
17140µs13µ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
24a page we generated. Validation keys are injected into all HTML pages
25generated by Foswiki, in Foswiki::writeCompletePage. When a request is
26received from the browser that requires validation, that request must
27be accompanied by the validation key. The functions in this package
28support the generation and checking of these validation keys.
29
30Two key validation methods are supported by this module; simple token
31validation, and double-submission validation. Simple token validation
32stores a magic number in the session, and then adds that magic number to
33all forms in the output HTML. When a form is submitted, the magic number
34submitted with the form must match the number stored in the session. This is
35a relatively weak protection method, but requires some coding around so may
36discourage many hackers.
37
38The second method supported is properly called double cookie submission,
39but referred to as "strikeone" in Foswiki. This again uses a token added
40to output forms, but this time it uses Javascript to combine that token
41with a secret stored in a cookie, to create a new token. This is more secure
42because the cookie containing the secret cannot be read outside the domain
43of the server, making it much harder for a page hosted on an evil site to
44forge a valid transaction.
45
46When a request requiring validation comes in, Foswiki::UI::checkValidationKey
47is called. This compares the key in the request with the set of valid keys
48stored in the session. If the comparison fails, the browser is redirected
49to the =login= script (even if the user is currently logged in) with the
50=action= parameter set to =validate=. This generates a confirmation screen
51that the user must accept before the transaction can proceed. When the screen
52is confirmed, =login= is invoked again and the original transaction restored
53from passthrough.
54
55In the function descriptions below, $cgis is a reference to a CGI::Session
56object.
57
58=cut
59
60# Set to 1 to trace validation steps in STDERR
6121.08ms268µ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
use constant TRACE => 0;
# 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
65sub _getSecretCookieName { 'FOSWIKISTRIKEONE' }
66
67=begin TML
68
69---++ StaticMethod addValidationKey( $cgis, $context, $strikeone ) -> $form
70
71Add 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.
78The validation key will be added as a hidden parameter at the end of
79the form tag.
80
81=cut
82
83sub 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
99Generate 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.
106The validation key can then be used in a HTML form, or headers for
107RestPlugin API etc.
108
109=cut
110
111# TODO: should this be callable from Foswiki::Func so that RestHandlers
112# can use it too?
113sub 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
154Add a double submission onsubmit handler to a form.
155 * =$form= - the opening tag of a form, ie. &lt;form ...&gt;=
156The handler will be added to an existing on submit, or by adding a new
157onsubmit in the form tag.
158
159=cut
160
161sub addOnSubmit {
162 my ($form) = @_;
163 unless ( $form =~
164s/\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
176Get a double submission cookie
177 * =$cgis= - a CGI::Session
178
179The cookie is a non-HttpOnly cookie that contains the current session ID
180and a secret. The secret is constant for a given session.
181
182=cut
183
184sub 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
206Check that the given validation key is valid for the session.
207Return false if not.
208
209=cut
210
211sub 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
221Check that the given validation key is valid for the session.
222Return false if not.
223
224=cut
225
226sub 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
241Expire any timed-out validation keys for this session, and (optionally)
242force expiry of a specific key, even if it hasn't timed out.
243
244=cut
245
246sub 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
292Generate (or check) the "Suspicious request" verification screen for the
293given session. This screen is generated when a validation fails, as a
294response to a ValidationException.
295
296=cut
297
298sub 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
369sub _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
38112µs1;
382__END__