Index: branches/REL1_3/phase3/includes/User.php |
— | — | @@ -738,6 +738,39 @@ |
739 | 739 | } |
740 | 740 | return false; |
741 | 741 | } |
| 742 | + |
| 743 | + /** |
| 744 | + * Initialize (if necessary) and return a session token value |
| 745 | + * which can be used in edit forms to show that the user's |
| 746 | + * login credentials aren't being hijacked with a foreign form |
| 747 | + * submission. |
| 748 | + * |
| 749 | + * @return string |
| 750 | + * @access public |
| 751 | + */ |
| 752 | + function editToken() { |
| 753 | + if( !isset( $_SESSION['wsEditToken'] ) ) { |
| 754 | + $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
| 755 | + $_SESSION['wsEditToken'] = $token; |
| 756 | + } |
| 757 | + return $_SESSION['wsEditToken']; |
| 758 | + } |
| 759 | + |
| 760 | + /** |
| 761 | + * Check given value against the token value stored in the session. |
| 762 | + * A match should confirm that the form was submitted from the |
| 763 | + * user's own login session, not a form submission from a third-party |
| 764 | + * site. |
| 765 | + * |
| 766 | + * @param string $val |
| 767 | + * @return bool |
| 768 | + * @access public |
| 769 | + */ |
| 770 | + function matchEditToken( $val ) { |
| 771 | + if( !isset( $_SESSION['wsEditToken'] ) ) |
| 772 | + return false; |
| 773 | + return $_SESSION['wsEditToken'] == $val; |
| 774 | + } |
742 | 775 | } |
743 | 776 | |
744 | 777 | ?> |
Index: branches/REL1_3/phase3/includes/DefaultSettings.php |
— | — | @@ -9,7 +9,7 @@ |
10 | 10 | # like $wgScriptPath, you must also localize everything that |
11 | 11 | # depends on it. |
12 | 12 | |
13 | | -$wgVersion = '1.3.9'; |
| 13 | +$wgVersion = '1.3.10'; |
14 | 14 | |
15 | 15 | $wgSitename = 'MediaWiki'; # Please customize! |
16 | 16 | $wgMetaNamespace = FALSE; # will be same as you set $wgSitename |
— | — | @@ -315,7 +315,7 @@ |
316 | 316 | |
317 | 317 | # This is the list of preferred extensions for uploading files. Uploading |
318 | 318 | # files with extensions not in this list will trigger a warning. |
319 | | -$wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg', 'ogg' ); |
| 319 | +$wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' ); |
320 | 320 | |
321 | 321 | # Files with these extensions will never be allowed as uploads. |
322 | 322 | $wgFileBlacklist = array( |
— | — | @@ -474,10 +474,14 @@ |
475 | 475 | $wgExtensionFunctions = array(); |
476 | 476 | |
477 | 477 | # Allow user Javascript page? |
478 | | -$wgAllowUserJs = true; |
| 478 | +# This enables a lot of neat customizations, but may |
| 479 | +# increase security risk to users and server load. |
| 480 | +$wgAllowUserJs = false; |
479 | 481 | |
480 | 482 | # Allow user Cascading Style Sheets (CSS)? |
481 | | -$wgAllowUserCss = true; |
| 483 | +# This enables a lot of neat customizations, but may |
| 484 | +# increase security risk to users and server load. |
| 485 | +$wgAllowUserCss = false; |
482 | 486 | |
483 | 487 | # Filter for Special:Randompage. Part of a WHERE clause |
484 | 488 | $wgExtraRandompageSQL = false; |
Index: branches/REL1_3/phase3/includes/SkinPHPTal.php |
— | — | @@ -531,12 +531,13 @@ |
532 | 532 | } |
533 | 533 | /* private */ function setupUserCssJs () { |
534 | 534 | global $wgRequest, $wgTitle; |
| 535 | + global $wgAllowUserCss, $wgAllowUserJs; |
535 | 536 | $action = $wgRequest->getText('action'); |
536 | 537 | # generated css |
537 | 538 | $this->usercss = '@import "'.$this->makeUrl('-','action=raw&gen=css').'";'."\n"; |
538 | 539 | |
539 | | - if( $this->loggedin ) { |
540 | | - if($wgTitle->isCssSubpage() and $action == 'submit' and $wgTitle->userCanEditCssJsSubpage()) { |
| 540 | + if( $this->loggedin && $wgAllowUserCss ) { |
| 541 | + if( $wgTitle->isCssSubpage() and $this->userCanPreview( $action ) ) { |
541 | 542 | # generated css |
542 | 543 | $this->usercss = '@import "'.$this->makeUrl('-','action=raw&smaxage=0&maxage=0&gen=css').'";'."\n"; |
543 | 544 | // css preview |
— | — | @@ -548,7 +549,9 @@ |
549 | 550 | $this->usercss .= '@import "'. |
550 | 551 | $this->makeUrl($this->userpage.'/'.$this->skinname.'.css', 'action=raw&ctype=text/css').'";'."\n"; |
551 | 552 | } |
552 | | - if($wgTitle->isJsSubpage() and $action == 'submit' and $wgTitle->userCanEditCssJsSubpage()) { |
| 553 | + } |
| 554 | + if( $this->loggedin && $wgAllowUserJs ) { |
| 555 | + if( $wgTitle->isJsSubpage() and $this->userCanPreview( $action ) ) { |
553 | 556 | # XXX: additional security check/prompt? |
554 | 557 | $this->userjsprev = $wgRequest->getText('wpTextbox1'); |
555 | 558 | } else { |
Index: branches/REL1_3/phase3/includes/Skin.php |
— | — | @@ -166,6 +166,30 @@ |
167 | 167 | return $r; |
168 | 168 | } |
169 | 169 | |
| 170 | + /** |
| 171 | + * To make it harder for someone to slip a user a fake |
| 172 | + * user-JavaScript or user-CSS preview, a random token |
| 173 | + * is associated with the login session. If it's not |
| 174 | + * passed back with the preview request, we won't render |
| 175 | + * the code. |
| 176 | + * |
| 177 | + * @param string $action |
| 178 | + * @return bool |
| 179 | + * @access private |
| 180 | + */ |
| 181 | + function userCanPreview( $action ) { |
| 182 | + global $wgTitle, $wgRequest, $wgUser; |
| 183 | + |
| 184 | + if( $action != 'submit' ) |
| 185 | + return false; |
| 186 | + if( !$wgRequest->wasPosted() ) |
| 187 | + return false; |
| 188 | + if( !$wgTitle->userCanEditCssJsSubpage() ) |
| 189 | + return false; |
| 190 | + return $wgUser->matchEditToken( |
| 191 | + $wgRequest->getVal( 'wpEditToken' ) ); |
| 192 | + } |
| 193 | + |
170 | 194 | # get the user/site-specific stylesheet, SkinPHPTal called from RawPage.php (settings are cached that way) |
171 | 195 | function getUserStylesheet() { |
172 | 196 | global $wgOut, $wgStylePath, $wgLang, $wgUser, $wgRequest, $wgTitle, $wgAllowUserCss; |
— | — | @@ -174,7 +198,7 @@ |
175 | 199 | $s = "@import \"$wgStylePath/$sheet\";\n"; |
176 | 200 | if($wgLang->isRTL()) $s .= "@import \"$wgStylePath/common_rtl.css\";\n"; |
177 | 201 | if( $wgAllowUserCss && $wgUser->getID() != 0 ) { # logged in |
178 | | - if($wgTitle->isCssSubpage() and $action == 'submit' and $wgTitle->userCanEditCssJsSubpage()) { |
| 202 | + if($wgTitle->isCssSubpage() && $this->userCanPreview( $action ) ) { |
179 | 203 | $s .= $wgRequest->getText('wpTextbox1'); |
180 | 204 | } else { |
181 | 205 | $userpage = $wgLang->getNsText( Namespace::getUser() ) . ":" . $wgUser->getName(); |
Index: branches/REL1_3/phase3/includes/EditPage.php |
— | — | @@ -73,7 +73,14 @@ |
74 | 74 | if( !preg_match( '/^\d{14}$/', $this->edittime )) $this->edittime = ""; |
75 | 75 | |
76 | 76 | $this->preview = $request->getCheck( 'wpPreview' ); |
77 | | - $this->save = $request->wasPosted() && !$this->preview; |
| 77 | + if( $this->tokenOk( $request ) ) { |
| 78 | + $this->save = $request->wasPosted() && !$this->preview; |
| 79 | + } else { |
| 80 | + # Page might be a hack attempt posted from |
| 81 | + # an external site. Preview instead of saving. |
| 82 | + $this->save = false; |
| 83 | + $this->preview = $request->wasPosted(); |
| 84 | + } |
78 | 85 | $this->minoredit = $request->getCheck( 'wpMinoredit' ); |
79 | 86 | $this->watchthis = $request->getCheck( 'wpWatchthis' ); |
80 | 87 | |
— | — | @@ -83,6 +90,24 @@ |
84 | 91 | $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); |
85 | 92 | } |
86 | 93 | |
| 94 | + /** |
| 95 | + * Make sure the form isn't faking a user's credentials. |
| 96 | + * |
| 97 | + * @param WebRequest $request |
| 98 | + * @return bool |
| 99 | + * @access private |
| 100 | + */ |
| 101 | + function tokenOk( &$request ) { |
| 102 | + global $wgUser; |
| 103 | + if( $wgUser->getId() == 0 ) { |
| 104 | + # Anonymous users may not have a session |
| 105 | + # open. Don't tokenize. |
| 106 | + return true; |
| 107 | + } else { |
| 108 | + return $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); |
| 109 | + } |
| 110 | + } |
| 111 | + |
87 | 112 | # Since there is only one text field on the edit form, |
88 | 113 | # pressing <enter> will cause the form to be submitted, but |
89 | 114 | # the submit button value won't appear in the query, so we |
— | — | @@ -235,6 +260,20 @@ |
236 | 261 | # Enabled article-related sidebar, toplinks, etc. |
237 | 262 | $wgOut->setArticleRelated( true ); |
238 | 263 | |
| 264 | + if ( 0 != $wgUser->getID() ) { |
| 265 | + /** |
| 266 | + * To make it harder for someone to slip a user a page |
| 267 | + * which submits an edit form to the wiki without their |
| 268 | + * knowledge, a random token is associated with the login |
| 269 | + * session. If it's not passed back with the submission, |
| 270 | + * we won't save the page, or render user JavaScript and |
| 271 | + * CSS previews. |
| 272 | + */ |
| 273 | + $token = htmlspecialchars( $wgUser->editToken() ); |
| 274 | + $wgOut->addHTML( " |
| 275 | +<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); |
| 276 | + } |
| 277 | + |
239 | 278 | if ( $isConflict ) { |
240 | 279 | $s = wfMsg( "editconflict", $this->mTitle->getPrefixedText() ); |
241 | 280 | $wgOut->setPageTitle( $s ); |
— | — | @@ -441,6 +480,20 @@ |
442 | 481 | <input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> |
443 | 482 | <input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n" ); |
444 | 483 | |
| 484 | + if ( 0 != $wgUser->getID() ) { |
| 485 | + /** |
| 486 | + * To make it harder for someone to slip a user a page |
| 487 | + * which submits an edit form to the wiki without their |
| 488 | + * knowledge, a random token is associated with the login |
| 489 | + * session. If it's not passed back with the submission, |
| 490 | + * we won't save the page, or render user JavaScript and |
| 491 | + * CSS previews. |
| 492 | + */ |
| 493 | + $token = htmlspecialchars( $wgUser->editToken() ); |
| 494 | + $wgOut->addHTML( " |
| 495 | +<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); |
| 496 | + } |
| 497 | + |
445 | 498 | if ( $isConflict ) { |
446 | 499 | $wgOut->addHTML( "<h2>" . wfMsg( "yourdiff" ) . "</h2>\n" ); |
447 | 500 | DifferenceEngine::showDiff( $this->textbox2, $this->textbox1, |
Index: branches/REL1_3/phase3/RELEASE-NOTES |
— | — | @@ -3,6 +3,34 @@ |
4 | 4 | Security reminder: MediaWiki does not require PHP's register_globals |
5 | 5 | setting since version 1.2.0. If you have it on, turn it *off* if you can. |
6 | 6 | |
| 7 | +== Version 1.3.10, 2005-02-03 == |
| 8 | + |
| 9 | +MediaWiki 1.3.10 is a security release. |
| 10 | + |
| 11 | +An attacker could craft a URL which, when visited by a particular |
| 12 | +logged-in user, would execute arbitrary JavaScript code on the user's |
| 13 | +browser in the wiki's site context. This attack has been blocked, and as |
| 14 | +an extra precaution the user CSS and JavaScript subpage support is now |
| 15 | +disabled by default. Sites which want this ability may set $wgAllowUserCss |
| 16 | +and $wgAllowUserJs in LocalSettings.php. |
| 17 | + |
| 18 | +Additional protections have been added against off-site form submissions |
| 19 | +hijacking user credentials. Authors of bot tools may need to update their |
| 20 | +code to include additional fields. |
| 21 | + |
| 22 | +All wikis running 1.3.x are strongly urged to upgrade to 1.3.10. |
| 23 | + |
| 24 | +Changes from 1.3.9: |
| 25 | +* Logged-in edits and preview of user CSS/JS are now locked to a session token. |
| 26 | +* Per-user CSS and JavaScript subpage customizations now disabled by default. |
| 27 | + They can be re-enabled via $wgAllowUserJs and $wgAllowUserCss. |
| 28 | +* Removed .ogg from the default uploads whitelist as an extra precaution. |
| 29 | + If your web server is configured to serve Ogg files with the correct |
| 30 | + Content-Type header, you can re-add it in LocalSettings.php: |
| 31 | + $wgFileExtensions[] = 'ogg'; |
| 32 | + |
| 33 | + |
| 34 | + |
7 | 35 | == Version 1.3.9, 2004-12-12 == |
8 | 36 | |
9 | 37 | MediaWiki 1.3.9 is a security and bug fix release. |