Filename | /var/www/foswiki11/lib/Foswiki/Validation.pm |
Statements | Executed 13 statements in 1.11ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
1 | 1 | 1 | 18µs | 30µs | BEGIN@4 | Foswiki::Validation::
1 | 1 | 1 | 8µs | 14µs | BEGIN@5 | Foswiki::Validation::
1 | 1 | 1 | 7µs | 20µs | BEGIN@7 | Foswiki::Validation::
1 | 1 | 1 | 7µs | 37µs | BEGIN@54 | Foswiki::Validation::
1 | 1 | 1 | 4µs | 4µs | BEGIN@9 | 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 | getCookie | Foswiki::Validation::
0 | 0 | 0 | 0s | 0s | isValidNonce | 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 | 28µs | 2 | 43µs | # spent 30µs (18+12) within Foswiki::Validation::BEGIN@4 which was called:
# once (18µs+12µs) by Foswiki::UI::BEGIN@160 at line 4 # spent 30µs making 1 call to Foswiki::Validation::BEGIN@4
# spent 12µs making 1 call to strict::import |
5 | 2 | 23µs | 2 | 19µs | # spent 14µs (8+5) within Foswiki::Validation::BEGIN@5 which was called:
# once (8µs+5µs) by Foswiki::UI::BEGIN@160 at line 5 # spent 14µs making 1 call to Foswiki::Validation::BEGIN@5
# spent 5µs making 1 call to warnings::import |
6 | |||||
7 | 2 | 22µs | 2 | 32µs | # spent 20µs (7+13) within Foswiki::Validation::BEGIN@7 which was called:
# once (7µs+13µs) by Foswiki::UI::BEGIN@160 at line 7 # spent 20µs making 1 call to Foswiki::Validation::BEGIN@7
# spent 13µs making 1 call to Assert::import |
8 | |||||
9 | 2 | 17µs | 1 | 4µs | # spent 4µs within Foswiki::Validation::BEGIN@9 which was called:
# once (4µs+0s) by Foswiki::UI::BEGIN@160 at line 9 # spent 4µs making 1 call to Foswiki::Validation::BEGIN@9 |
10 | 2 | 40µs | 1 | 3µs | # spent 3µs within Foswiki::Validation::BEGIN@10 which was called:
# once (3µs+0s) by Foswiki::UI::BEGIN@160 at line 10 # spent 3µ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 | 982µs | 2 | 68µs | # spent 37µs (7+30) within Foswiki::Validation::BEGIN@54 which was called:
# once (7µs+30µs) by Foswiki::UI::BEGIN@160 at line 54 # spent 37µs making 1 call to Foswiki::Validation::BEGIN@54
# spent 30µ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 | 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 | sub addValidationKey { | ||||
77 | my ( $cgis, $context, $strikeone ) = @_; | ||||
78 | my $actions = $cgis->param('VALID_ACTIONS') || {}; | ||||
79 | my $nonce = Digest::MD5::md5_hex( $context, $cgis->id() ); | ||||
80 | my $action = $nonce; | ||||
81 | if ($strikeone) { | ||||
82 | |||||
83 | # When using strikeone, the validation key pushed into the form will | ||||
84 | # be combined with the secret in the cookie, and the combination | ||||
85 | # will be md5 encoded before sending back. Since we know the secret | ||||
86 | # and the validation key, then might as well save the hashed version. | ||||
87 | # This has to be consistent with the algorithm in strikeone.js | ||||
88 | my $secret = _getSecret($cgis); | ||||
89 | $action = Digest::MD5::md5_hex( $nonce, $secret ); | ||||
90 | |||||
91 | #print STDERR "V: STRIKEONE $nonce + $secret = $action\n" if TRACE; | ||||
92 | } | ||||
93 | my $timeout = time() + $Foswiki::cfg{Validation}{ValidForTime}; | ||||
94 | print STDERR "V: ADD KEY $action" | ||||
95 | . ( $nonce ne $action ? "($nonce)" : '' ) . ' = ' | ||||
96 | . $timeout . "\n" | ||||
97 | if TRACE && !defined $actions->{$action}; | ||||
98 | $actions->{$action} = $timeout; | ||||
99 | |||||
100 | $cgis->param( 'VALID_ACTIONS', $actions ); | ||||
101 | |||||
102 | # Don't use CGI::hidden; it will inherit the URL param value of | ||||
103 | # validation key and override our value :-( | ||||
104 | return "<input type='hidden' name='validation_key' value='?$nonce' />"; | ||||
105 | } | ||||
106 | |||||
107 | =begin TML | ||||
108 | |||||
109 | ---++ StaticMethod addOnSubmit( $form ) -> $form | ||||
110 | |||||
111 | Add a double submission onsubmit handler to a form. | ||||
112 | * =$form= - the opening tag of a form, ie. <form ...>= | ||||
113 | The handler will be added to an existing on submit, or by adding a new | ||||
114 | onsubmit in the form tag. | ||||
115 | |||||
116 | =cut | ||||
117 | |||||
118 | sub addOnSubmit { | ||||
119 | my ($form) = @_; | ||||
120 | unless ( $form =~ | ||||
121 | s/\bonsubmit=(["'])((?:\s*javascript:)?)(.*)\1/onsubmit=${1}${2}StrikeOne.submit(this);$3$1/i | ||||
122 | ) | ||||
123 | { | ||||
124 | $form =~ s/>$/ onsubmit="StrikeOne.submit(this)">/; | ||||
125 | } | ||||
126 | return $form; | ||||
127 | } | ||||
128 | |||||
129 | =begin TML | ||||
130 | |||||
131 | ---++ StaticMethod getCookie( $cgis ) -> $cookie | ||||
132 | |||||
133 | Get a double submission cookie | ||||
134 | * =$cgis= - a CGI::Session | ||||
135 | |||||
136 | The cookie is a non-HttpOnly cookie that contains the current session ID | ||||
137 | and a secret. The secret is constant for a given session. | ||||
138 | |||||
139 | The caller should adjust the =-secure= flag of the cookie, according to the | ||||
140 | request being processed. | ||||
141 | |||||
142 | =cut | ||||
143 | |||||
144 | sub getCookie { | ||||
145 | my ($cgis) = @_; | ||||
146 | |||||
147 | my $secret = _getSecret($cgis); | ||||
148 | |||||
149 | # Add the cookie to the response | ||||
150 | require CGI::Cookie; | ||||
151 | my $cookie = CGI::Cookie->new( | ||||
152 | -name => _getSecretCookieName(), | ||||
153 | -value => $secret, | ||||
154 | -path => '/', | ||||
155 | -httponly => 0, # we *want* JS to be able to read it! | ||||
156 | ); | ||||
157 | |||||
158 | return $cookie; | ||||
159 | } | ||||
160 | |||||
161 | =begin TML | ||||
162 | |||||
163 | ---++ StaticMethod isValidNonce( $cgis, $key ) -> $boolean | ||||
164 | |||||
165 | Check that the given validation key is valid for the session. | ||||
166 | Return false if not. | ||||
167 | |||||
168 | =cut | ||||
169 | |||||
170 | sub isValidNonce { | ||||
171 | my ( $cgis, $nonce ) = @_; | ||||
172 | return 1 if ( $Foswiki::cfg{Validation}{Method} eq 'none' ); | ||||
173 | return 0 unless defined $nonce; | ||||
174 | $nonce =~ s/^\?// if ( $Foswiki::cfg{Validation}{Method} ne 'strikeone' ); | ||||
175 | my $actions = $cgis->param('VALID_ACTIONS'); | ||||
176 | return 0 unless ref($actions) eq 'HASH'; | ||||
177 | print STDERR "V: CHECK $nonce -> " . ( $actions->{$nonce} ? 1 : 0 ) . "\n" | ||||
178 | if TRACE; | ||||
179 | return $actions->{$nonce}; | ||||
180 | } | ||||
181 | |||||
182 | =begin TML | ||||
183 | |||||
184 | ---++ StaticMethod expireValidationKeys($cgis[, $key]) | ||||
185 | |||||
186 | Expire any timed-out validation keys for this session, and (optionally) | ||||
187 | force expiry of a specific key, even if it hasn't timed out. | ||||
188 | |||||
189 | =cut | ||||
190 | |||||
191 | sub expireValidationKeys { | ||||
192 | my ( $cgis, $key ) = @_; | ||||
193 | my $actions = $cgis->param('VALID_ACTIONS'); | ||||
194 | if ($actions) { | ||||
195 | if ( defined $key && exists $actions->{$key} ) { | ||||
196 | $actions->{$key} = 0; # force-expire this key | ||||
197 | } | ||||
198 | my $deaths = 0; | ||||
199 | my $now = time(); | ||||
200 | while ( my ( $nonce, $time ) = each %$actions ) { | ||||
201 | if ( $time < $now ) { | ||||
202 | |||||
203 | print STDERR "V: EXPIRE $nonce $time\n" if TRACE; | ||||
204 | delete $actions->{$nonce}; | ||||
205 | $deaths++; | ||||
206 | } | ||||
207 | } | ||||
208 | |||||
209 | # If we have more than the permitted number of keys, expire | ||||
210 | # the oldest ones. | ||||
211 | my $excess = | ||||
212 | scalar( keys %$actions ) - | ||||
213 | $Foswiki::cfg{Validation}{MaxKeysPerSession}; | ||||
214 | if ( $excess > 0 ) { | ||||
215 | print STDERR "V: $excess TOO MANY KEYS\n" if TRACE; | ||||
216 | my @keys = sort { $actions->{$a} <=> $actions->{$b} } | ||||
217 | keys %$actions; | ||||
218 | while ( $excess-- > 0 ) { | ||||
219 | my $key = shift(@keys); | ||||
220 | print STDERR "V: EXPIRE $key $actions->{$key}\n" if TRACE; | ||||
221 | delete $actions->{$key}; | ||||
222 | $deaths++; | ||||
223 | } | ||||
224 | } | ||||
225 | if ($deaths) { | ||||
226 | $cgis->param( 'VALID_ACTIONS', $actions ); | ||||
227 | } | ||||
228 | } | ||||
229 | } | ||||
230 | |||||
231 | =begin TML | ||||
232 | |||||
233 | ---++ StaticMethod validate($session) | ||||
234 | |||||
235 | Generate (or check) the "Suspicious request" verification screen for the | ||||
236 | given session. This screen is generated when a validation fails, as a | ||||
237 | response to a ValidationException. | ||||
238 | |||||
239 | =cut | ||||
240 | |||||
241 | sub validate { | ||||
242 | my ($session) = @_; | ||||
243 | my $query = $session->{request}; | ||||
244 | my $web = $session->{webName}; | ||||
245 | my $topic = $session->{topicName}; | ||||
246 | my $cgis = $session->getCGISession(); | ||||
247 | |||||
248 | my $tmpl = $session->templates->readTemplate('validate'); | ||||
249 | |||||
250 | if ( $query->param('response') ) { | ||||
251 | my $cacheUID = $query->param('foswikioriginalquery'); | ||||
252 | $query->delete('foswikioriginalquery'); | ||||
253 | my $url; | ||||
254 | if ( $query->param('response') eq 'OK' | ||||
255 | && isValidNonce( $cgis, $query->param('validation_key') ) ) | ||||
256 | { | ||||
257 | if ( !$cacheUID ) { | ||||
258 | $url = $session->getScriptUrl( 0, 'view', $web, $topic ); | ||||
259 | } | ||||
260 | else { | ||||
261 | |||||
262 | # Reload the cached original query over the current query. | ||||
263 | # When the redirect is validated it should pass, because | ||||
264 | # it will now be using the validation code from the | ||||
265 | # confirmation screen that brought us here. | ||||
266 | require Foswiki::Request::Cache; | ||||
267 | Foswiki::Request::Cache->new()->load( $cacheUID, $query ); | ||||
268 | $url = $query->url(); | ||||
269 | } | ||||
270 | |||||
271 | # Complete the query by passing the query on | ||||
272 | # with passthrough | ||||
273 | print STDERR "WV: CONFIRMED; POST to $url\n" if TRACE; | ||||
274 | $session->redirect( $url, 1 ); | ||||
275 | } | ||||
276 | else { | ||||
277 | print STDERR "V: CONFIRMATION REJECTED\n" if TRACE; | ||||
278 | |||||
279 | # Validation failed; redirect to view (302) | ||||
280 | $url = $session->getScriptUrl( 0, 'view', $web, $topic ); | ||||
281 | $session->redirect( $url, 0 ); # no passthrough | ||||
282 | } | ||||
283 | } | ||||
284 | else { | ||||
285 | |||||
286 | print STDERR "V: PROMPTING FOR CONFIRMATION " . $query->uri() . "\n" | ||||
287 | if TRACE; | ||||
288 | |||||
289 | # Prompt for user verification - code 419 chosen by foswiki devs. | ||||
290 | # None of the defined HTTP codes describe what is really happening, | ||||
291 | # which is why we chose a "new" code. The confirmation page | ||||
292 | # isn't a conflict, not a security issue, and we cannot use 403 | ||||
293 | # because there is a high probability this would get caught by | ||||
294 | # Apache to send back the Registation page. We didn't want any | ||||
295 | # installation to catch the HTTP return code we were sending back, | ||||
296 | # as we need this page to arrive intact to the user, otherwise | ||||
297 | # they won't be able to do anything. 419 is a placebo, and if it | ||||
298 | # is ever defined can be replaced by any other undefined 4xx code. | ||||
299 | $session->{response}->status(419); | ||||
300 | |||||
301 | my $topicObject = Foswiki::Meta->new( $session, $web, $topic ); | ||||
302 | $tmpl = $topicObject->expandMacros($tmpl); | ||||
303 | $tmpl = $topicObject->renderTML($tmpl); | ||||
304 | $tmpl =~ s/<nop>//g; | ||||
305 | |||||
306 | $session->writeCompletePage($tmpl); | ||||
307 | } | ||||
308 | } | ||||
309 | |||||
310 | # Get/set the one-strike secret in the CGI::Session | ||||
311 | sub _getSecret { | ||||
312 | my $cgis = shift; | ||||
313 | my $secret = $cgis->param( _getSecretCookieName() ); | ||||
314 | unless ($secret) { | ||||
315 | |||||
316 | # Use hex encoding to make it cookie-friendly | ||||
317 | $secret = Digest::MD5::md5_hex( $cgis->id(), rand(time) ); | ||||
318 | $cgis->param( _getSecretCookieName(), $secret ); | ||||
319 | } | ||||
320 | return $secret; | ||||
321 | } | ||||
322 | |||||
323 | 1 | 2µs | 1; | ||
324 | __END__ |