Index: trunk/phase3/includes/diff/DifferenceEngine.php |
— | — | @@ -115,6 +115,8 @@ |
116 | 116 | global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; |
117 | 117 | wfProfileIn( __METHOD__ ); |
118 | 118 | |
| 119 | + # Allow frames except in certain special cases |
| 120 | + $wgOut->allowClickjacking(); |
119 | 121 | |
120 | 122 | # If external diffs are enabled both globally and for the user, |
121 | 123 | # we'll use the application/x-external-editor interface to call |
— | — | @@ -206,6 +208,7 @@ |
207 | 209 | // Check if page is editable |
208 | 210 | $editable = $this->mNewRev->getTitle()->userCan( 'edit' ); |
209 | 211 | if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) { |
| 212 | + $wgOut->preventClickjacking(); |
210 | 213 | $rollback = '   ' . $sk->generateRollback( $this->mNewRev ); |
211 | 214 | } else { |
212 | 215 | $rollback = ''; |
— | — | @@ -243,6 +246,7 @@ |
244 | 247 | } |
245 | 248 | // Build the link |
246 | 249 | if ( $rcid ) { |
| 250 | + $wgOut->preventClickjacking(); |
247 | 251 | $token = $wgUser->editToken( $rcid ); |
248 | 252 | $patrol = ' <span class="patrollink">[' . $sk->link( |
249 | 253 | $this->mTitle, |
— | — | @@ -471,6 +475,7 @@ |
472 | 476 | if ( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan( 'patrol' ) ) { |
473 | 477 | $sk = $wgUser->getSkin(); |
474 | 478 | $token = $wgUser->editToken( $this->mRcidMarkPatrolled ); |
| 479 | + $wgOut->preventClickjacking(); |
475 | 480 | $wgOut->addHTML( |
476 | 481 | "<div class='patrollink'>[" . $sk->link( |
477 | 482 | $this->mTitle, |
Index: trunk/phase3/includes/Article.php |
— | — | @@ -886,6 +886,9 @@ |
887 | 887 | return; |
888 | 888 | } |
889 | 889 | |
| 890 | + # Allow frames by default |
| 891 | + $wgOut->allowClickjacking(); |
| 892 | + |
890 | 893 | if ( !$wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) { |
891 | 894 | $parserOptions->setEditSection( false ); |
892 | 895 | } |
— | — | @@ -1304,6 +1307,7 @@ |
1305 | 1308 | |
1306 | 1309 | $sk = $wgUser->getSkin(); |
1307 | 1310 | $token = $wgUser->editToken( $rcid ); |
| 1311 | + $wgOut->preventClickjacking(); |
1308 | 1312 | |
1309 | 1313 | $wgOut->addHTML( |
1310 | 1314 | "<div class='patrollink'>" . |
— | — | @@ -1584,6 +1588,8 @@ |
1585 | 1589 | return; |
1586 | 1590 | } |
1587 | 1591 | |
| 1592 | + $wgOut->preventClickjacking(); |
| 1593 | + |
1588 | 1594 | $tbtext = ""; |
1589 | 1595 | foreach ( $tbs as $o ) { |
1590 | 1596 | $rmvtxt = ""; |
Index: trunk/phase3/includes/ImagePage.php |
— | — | @@ -601,6 +601,7 @@ |
602 | 602 | $this->loadFile(); |
603 | 603 | $pager = new ImageHistoryPseudoPager( $this ); |
604 | 604 | $wgOut->addHTML( $pager->getBody() ); |
| 605 | + $wgOut->preventClickjacking( $pager->getPreventClickjacking() ); |
605 | 606 | |
606 | 607 | $this->img->resetHistory(); // free db resources |
607 | 608 | |
— | — | @@ -828,6 +829,7 @@ |
829 | 830 | class ImageHistoryList { |
830 | 831 | |
831 | 832 | protected $imagePage, $img, $skin, $title, $repo, $showThumb; |
| 833 | + protected $preventClickjacking = false; |
832 | 834 | |
833 | 835 | public function __construct( $imagePage ) { |
834 | 836 | global $wgUser, $wgShowArchiveThumbnails; |
— | — | @@ -954,6 +956,7 @@ |
955 | 957 | # Don't link to unviewable files |
956 | 958 | $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>'; |
957 | 959 | } elseif ( $file->isDeleted( File::DELETED_FILE ) ) { |
| 960 | + $this->preventClickjacking(); |
958 | 961 | $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); |
959 | 962 | # Make a link to review the image |
960 | 963 | $url = $this->skin->link( |
— | — | @@ -1041,9 +1044,19 @@ |
1042 | 1045 | return wfMsgHtml( 'filehist-nothumb' ); |
1043 | 1046 | } |
1044 | 1047 | } |
| 1048 | + |
| 1049 | + protected function preventClickjacking( $enable = true ) { |
| 1050 | + $this->preventClickjacking = $enable; |
| 1051 | + } |
| 1052 | + |
| 1053 | + public function getPreventClickjacking() { |
| 1054 | + return $this->preventClickjacking; |
| 1055 | + } |
1045 | 1056 | } |
1046 | 1057 | |
1047 | 1058 | class ImageHistoryPseudoPager extends ReverseChronologicalPager { |
| 1059 | + protected $preventClickjacking = false; |
| 1060 | + |
1048 | 1061 | function __construct( $imagePage ) { |
1049 | 1062 | parent::__construct(); |
1050 | 1063 | $this->mImagePage = $imagePage; |
— | — | @@ -1084,6 +1097,10 @@ |
1085 | 1098 | $s .= $list->imageHistoryLine( !$file->isOld(), $file ); |
1086 | 1099 | } |
1087 | 1100 | $s .= $list->endImageHistoryList( $navLink ); |
| 1101 | + |
| 1102 | + if ( $list->getPreventClickjacking() ) { |
| 1103 | + $this->preventClickjacking(); |
| 1104 | + } |
1088 | 1105 | } |
1089 | 1106 | return $s; |
1090 | 1107 | } |
— | — | @@ -1166,4 +1183,13 @@ |
1167 | 1184 | } |
1168 | 1185 | $this->mQueryDone = true; |
1169 | 1186 | } |
| 1187 | + |
| 1188 | + protected function preventClickjacking( $enable = true ) { |
| 1189 | + $this->preventClickjacking = $enable; |
| 1190 | + } |
| 1191 | + |
| 1192 | + public function getPreventClickjacking() { |
| 1193 | + return $this->preventClickjacking; |
| 1194 | + } |
| 1195 | + |
1170 | 1196 | } |
Index: trunk/phase3/includes/HTMLForm.php |
— | — | @@ -342,6 +342,9 @@ |
343 | 343 | function displayForm( $submitResult ) { |
344 | 344 | global $wgOut; |
345 | 345 | |
| 346 | + # For good measure (it is the default) |
| 347 | + $wgOut->preventClickjacking(); |
| 348 | + |
346 | 349 | $html = '' |
347 | 350 | . $this->getErrors( $submitResult ) |
348 | 351 | . $this->mHeader |
Index: trunk/phase3/includes/OutputPage.php |
— | — | @@ -48,6 +48,7 @@ |
49 | 49 | var $mPageTitleActionText = ''; |
50 | 50 | var $mParseWarnings = array(); |
51 | 51 | var $mSquidMaxage = 0; |
| 52 | + var $mPreventClickjacking = true; |
52 | 53 | var $mRevisionId = null; |
53 | 54 | protected $mTitle = null; |
54 | 55 | |
— | — | @@ -1443,6 +1444,41 @@ |
1444 | 1445 | } |
1445 | 1446 | |
1446 | 1447 | /** |
| 1448 | + * Set a flag which will cause an X-Frame-Options header appropriate for |
| 1449 | + * edit pages to be sent. The header value is controlled by |
| 1450 | + * $wgEditPageFrameOptions. |
| 1451 | + * |
| 1452 | + * This is the default for special pages. If you display a CSRF-protected |
| 1453 | + * form on an ordinary view page, then you need to call this function. |
| 1454 | + */ |
| 1455 | + public function preventClickjacking( $enable = true ) { |
| 1456 | + $this->mPreventClickjacking = $enable; |
| 1457 | + } |
| 1458 | + |
| 1459 | + /** |
| 1460 | + * Turn off frame-breaking. Alias for $this->preventClickjacking(false). |
| 1461 | + * This can be called from pages which do not contain any CSRF-protected |
| 1462 | + * HTML form. |
| 1463 | + */ |
| 1464 | + public function allowClickjacking() { |
| 1465 | + $this->mPreventClickjacking = false; |
| 1466 | + } |
| 1467 | + |
| 1468 | + /** |
| 1469 | + * Get the X-Frame-Options header value (without the name part), or false |
| 1470 | + * if there isn't one. This is used by Skin to determine whether to enable |
| 1471 | + * JavaScript frame-breaking, for clients that don't support X-Frame-Options. |
| 1472 | + */ |
| 1473 | + public function getFrameOptions() { |
| 1474 | + global $wgBreakFrames, $wgEditPageFrameOptions; |
| 1475 | + if ( $wgBreakFrames ) { |
| 1476 | + return 'DENY'; |
| 1477 | + } elseif ( $this->mPreventClickjacking && $wgEditPageFrameOptions ) { |
| 1478 | + return $wgEditPageFrameOptions; |
| 1479 | + } |
| 1480 | + } |
| 1481 | + |
| 1482 | + /** |
1447 | 1483 | * Send cache control HTTP headers |
1448 | 1484 | */ |
1449 | 1485 | public function sendCacheControl() { |
— | — | @@ -1665,6 +1701,12 @@ |
1666 | 1702 | $wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); |
1667 | 1703 | $wgRequest->response()->header( 'Content-language: ' . $wgLanguageCode ); |
1668 | 1704 | |
| 1705 | + // Prevent framing, if requested |
| 1706 | + $frameOptions = $this->getFrameOptions(); |
| 1707 | + if ( $frameOptions ) { |
| 1708 | + $wgRequest->response()->header( "X-Frame-Options: $frameOptions" ); |
| 1709 | + } |
| 1710 | + |
1669 | 1711 | if ( $this->mArticleBodyOnly ) { |
1670 | 1712 | $this->out( $this->mBodytext ); |
1671 | 1713 | } else { |
Index: trunk/phase3/includes/HistoryPage.php |
— | — | @@ -171,6 +171,7 @@ |
172 | 172 | $pager->getBody() . |
173 | 173 | $pager->getNavigationBar() |
174 | 174 | ); |
| 175 | + $wgOut->preventClickjacking( $pager->getPreventClickjacking() ); |
175 | 176 | |
176 | 177 | wfProfileOut( __METHOD__ ); |
177 | 178 | } |
— | — | @@ -309,6 +310,7 @@ |
310 | 311 | class HistoryPager extends ReverseChronologicalPager { |
311 | 312 | public $lastRow = false, $counter, $historyPage, $title, $buttons, $conds; |
312 | 313 | protected $oldIdChecked; |
| 314 | + protected $preventClickjacking = false; |
313 | 315 | |
314 | 316 | function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = array() ) { |
315 | 317 | parent::__construct(); |
— | — | @@ -399,6 +401,7 @@ |
400 | 402 | ) . "\n"; |
401 | 403 | |
402 | 404 | if ( $wgUser->isAllowed( 'deleterevision' ) ) { |
| 405 | + $this->preventClickjacking(); |
403 | 406 | $float = $wgContLang->alignEnd(); |
404 | 407 | # Note bug #20966, <button> is non-standard in IE<8 |
405 | 408 | $element = Html::element( 'button', |
— | — | @@ -415,6 +418,7 @@ |
416 | 419 | $this->buttons .= $element; |
417 | 420 | } |
418 | 421 | if ( $wgUser->isAllowed( 'revisionmove' ) ) { |
| 422 | + $this->preventClickjacking(); |
419 | 423 | $float = $wgContLang->alignEnd(); |
420 | 424 | # Note bug #20966, <button> is non-standard in IE<8 |
421 | 425 | $element = Html::element( 'button', |
— | — | @@ -516,6 +520,7 @@ |
517 | 521 | $del = ''; |
518 | 522 | // Show checkboxes for each revision |
519 | 523 | if ( $wgUser->isAllowed( 'deleterevision' ) || $wgUser->isAllowed( 'revisionmove' ) ) { |
| 524 | + $this->preventClickjacking(); |
520 | 525 | // If revision was hidden from sysops, disable the checkbox |
521 | 526 | // However, if the user has revisionmove rights, we cannot disable the checkbox |
522 | 527 | if ( !$rev->userCan( Revision::DELETED_RESTRICTED ) && !$wgUser->isAllowed( 'revisionmove' ) ) { |
— | — | @@ -565,6 +570,7 @@ |
566 | 571 | # Rollback and undo links |
567 | 572 | if ( !is_null( $next ) && is_object( $next ) ) { |
568 | 573 | if ( $latest && $this->title->userCan( 'rollback' ) && $this->title->userCan( 'edit' ) ) { |
| 574 | + $this->preventClickjacking(); |
569 | 575 | $tools[] = '<span class="mw-rollback-link">' . |
570 | 576 | $this->getSkin()->buildRollbackLink( $rev ) . '</span>'; |
571 | 577 | } |
— | — | @@ -754,6 +760,20 @@ |
755 | 761 | return ''; |
756 | 762 | } |
757 | 763 | } |
| 764 | + |
| 765 | + /** |
| 766 | + * This is called if a write operation is possible from the generated HTML |
| 767 | + */ |
| 768 | + function preventClickjacking( $enable = true ) { |
| 769 | + $this->preventClickjacking = $enable; |
| 770 | + } |
| 771 | + |
| 772 | + /** |
| 773 | + * Get the "prevent clickjacking" flag |
| 774 | + */ |
| 775 | + function getPreventClickjacking() { |
| 776 | + return $this->preventClickjacking; |
| 777 | + } |
758 | 778 | } |
759 | 779 | |
760 | 780 | /** |
Index: trunk/phase3/includes/installer/WebInstallerOutput.php |
— | — | @@ -162,7 +162,8 @@ |
163 | 163 | $this->headerDone = true; |
164 | 164 | $dbTypes = $this->parent->getDBTypes(); |
165 | 165 | |
166 | | - $this->parent->request->response()->header("Content-Type: text/html; charset=utf-8"); |
| 166 | + $this->parent->request->response()->header( 'Content-Type: text/html; charset=utf-8' ); |
| 167 | + $this->parent->request->response()->header( 'X-Frame-Options: DENY' ); |
167 | 168 | if ( $this->redirectTarget ) { |
168 | 169 | $this->parent->request->response()->header( 'Location: '.$this->redirectTarget ); |
169 | 170 | return; |
Index: trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php |
— | — | @@ -30,7 +30,7 @@ |
31 | 31 | |
32 | 32 | protected function getConfig( $context ) { |
33 | 33 | global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, |
34 | | - $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames, |
| 34 | + $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, |
35 | 35 | $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, |
36 | 36 | $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, |
37 | 37 | $wgSitename, $wgFileExtensions; |
— | — | @@ -66,7 +66,6 @@ |
67 | 67 | 'wgServer' => $wgServer, |
68 | 68 | 'wgUserLanguage' => $context->getLanguage(), |
69 | 69 | 'wgContentLanguage' => $wgContLang->getCode(), |
70 | | - 'wgBreakFrames' => $wgBreakFrames, |
71 | 70 | 'wgVersion' => $wgVersion, |
72 | 71 | 'wgEnableAPI' => $wgEnableAPI, |
73 | 72 | 'wgEnableWriteAPI' => $wgEnableWriteAPI, |
Index: trunk/phase3/includes/DefaultSettings.php |
— | — | @@ -2261,12 +2261,33 @@ |
2262 | 2262 | $wgEnableTooltipsAndAccesskeys = true; |
2263 | 2263 | |
2264 | 2264 | /** |
2265 | | - * Break out of framesets. This can be used to prevent external sites from |
2266 | | - * framing your site with ads. |
| 2265 | + * Break out of framesets. This can be used to prevent clickjacking attacks, |
| 2266 | + * or to prevent external sites from framing your site with ads. |
2267 | 2267 | */ |
2268 | 2268 | $wgBreakFrames = false; |
2269 | 2269 | |
2270 | 2270 | /** |
| 2271 | + * The X-Frame-Options header to send on pages sensitive to clickjacking |
| 2272 | + * attacks, such as edit pages. This prevents those pages from being displayed |
| 2273 | + * in a frame or iframe. The options are: |
| 2274 | + * |
| 2275 | + * - 'DENY': Do not allow framing. This is recommended for most wikis. |
| 2276 | + * |
| 2277 | + * - 'SAMEORIGIN': Allow framing by pages on the same domain. This can be used |
| 2278 | + * to allow framing within a trusted domain. This is insecure if there |
| 2279 | + * is a page on the same domain which allows framing of arbitrary URLs. |
| 2280 | + * |
| 2281 | + * - false: Allow all framing. This opens up the wiki to XSS attacks and thus |
| 2282 | + * full compromise of local user accounts. Private wikis behind a |
| 2283 | + * corporate firewall are especially vulnerable. This is not |
| 2284 | + * recommended. |
| 2285 | + * |
| 2286 | + * For extra safety, set $wgBreakFrames = true, to prevent framing on all pages, |
| 2287 | + * not just edit pages. |
| 2288 | + */ |
| 2289 | +$wgEditPageFrameOptions = 'DENY'; |
| 2290 | + |
| 2291 | +/** |
2271 | 2292 | * Disable output compression (enabled by default if zlib is available) |
2272 | 2293 | */ |
2273 | 2294 | $wgDisableOutputCompression = false; |
Index: trunk/phase3/includes/specials/SpecialAllpages.php |
— | — | @@ -62,6 +62,7 @@ |
63 | 63 | |
64 | 64 | $this->setHeaders(); |
65 | 65 | $this->outputHeader(); |
| 66 | + $wgOut->allowClickjacking(); |
66 | 67 | |
67 | 68 | # GET values |
68 | 69 | $from = $wgRequest->getVal( 'from', null ); |
Index: trunk/phase3/includes/specials/SpecialCategories.php |
— | — | @@ -35,6 +35,7 @@ |
36 | 36 | |
37 | 37 | $this->setHeaders(); |
38 | 38 | $this->outputHeader(); |
| 39 | + $wgOut->allowClickjacking(); |
39 | 40 | |
40 | 41 | $from = $wgRequest->getText( 'from', $par ); |
41 | 42 | |
Index: trunk/phase3/includes/specials/SpecialSpecialpages.php |
— | — | @@ -33,8 +33,10 @@ |
34 | 34 | } |
35 | 35 | |
36 | 36 | function execute( $par ) { |
| 37 | + global $wgOut; |
37 | 38 | $this->setHeaders(); |
38 | 39 | $this->outputHeader(); |
| 40 | + $wgOut->allowClickjacking(); |
39 | 41 | |
40 | 42 | $groups = $this->getPageGroups(); |
41 | 43 | |
Index: trunk/phase3/includes/specials/SpecialContributions.php |
— | — | @@ -143,6 +143,7 @@ |
144 | 144 | '<p>' . $pager->getNavigationBar() . '</p>' |
145 | 145 | ); |
146 | 146 | } |
| 147 | + $wgOut->preventClickjacking( $pager->getPreventClickjacking() ); |
147 | 148 | |
148 | 149 | |
149 | 150 | # Show the appropriate "footer" message - WHOIS tools, etc. |
— | — | @@ -485,6 +486,7 @@ |
486 | 487 | public $mDefaultDirection = true; |
487 | 488 | var $messages, $target; |
488 | 489 | var $namespace = '', $mDb; |
| 490 | + var $preventClickjacking = false; |
489 | 491 | |
490 | 492 | function __construct( $options ) { |
491 | 493 | parent::__construct(); |
— | — | @@ -633,6 +635,7 @@ |
634 | 636 | if( !$row->page_is_new && $page->quickUserCan( 'rollback' ) |
635 | 637 | && $page->quickUserCan( 'edit' ) ) |
636 | 638 | { |
| 639 | + $this->preventClickjacking(); |
637 | 640 | $topmarktext .= ' '.$sk->generateRollback( $rev ); |
638 | 641 | } |
639 | 642 | } |
— | — | @@ -753,4 +756,11 @@ |
754 | 757 | } |
755 | 758 | } |
756 | 759 | |
| 760 | + protected function preventClickjacking() { |
| 761 | + $this->preventClickjacking = true; |
| 762 | + } |
| 763 | + |
| 764 | + public function getPreventClickjacking() { |
| 765 | + return $this->preventClickjacking; |
| 766 | + } |
757 | 767 | } |
Index: trunk/phase3/includes/specials/SpecialVersion.php |
— | — | @@ -53,6 +53,7 @@ |
54 | 54 | |
55 | 55 | $this->setHeaders(); |
56 | 56 | $this->outputHeader(); |
| 57 | + $wgOut->allowClickjacking(); |
57 | 58 | |
58 | 59 | $wgOut->addHTML( Xml::openElement( 'div', |
59 | 60 | array( 'dir' => $wgContLang->getDir() ) ) ); |
Index: trunk/phase3/includes/specials/SpecialSearch.php |
— | — | @@ -39,10 +39,11 @@ |
40 | 40 | * @param $par String or null |
41 | 41 | */ |
42 | 42 | public function execute( $par ) { |
43 | | - global $wgRequest, $wgUser; |
| 43 | + global $wgRequest, $wgUser, $wgOut; |
44 | 44 | |
45 | 45 | $this->setHeaders(); |
46 | 46 | $this->outputHeader(); |
| 47 | + $wgOut->allowClickjacking(); |
47 | 48 | |
48 | 49 | // Strip underscores from title parameter; most of the time we'll want |
49 | 50 | // text form here. But don't strip underscores from actual text params! |
Index: trunk/phase3/includes/specials/SpecialLinkSearch.php |
— | — | @@ -45,6 +45,7 @@ |
46 | 46 | function execute( $par ) { |
47 | 47 | global $wgOut, $wgRequest, $wgUrlProtocols, $wgMiserMode, $wgLang; |
48 | 48 | $this->setHeaders(); |
| 49 | + $wgOut->allowClickjacking(); |
49 | 50 | |
50 | 51 | $target = $wgRequest->getVal( 'target', $par ); |
51 | 52 | $namespace = $wgRequest->getIntorNull( 'namespace', null ); |
Index: trunk/phase3/includes/Skin.php |
— | — | @@ -502,6 +502,7 @@ |
503 | 503 | 'wgUserGroups' => $wgUser->getEffectiveGroups(), |
504 | 504 | 'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0, |
505 | 505 | 'wgCategories' => $wgOut->getCategories(), |
| 506 | + 'wgBreakFrames' => $wgOut->getFrameOptions() == 'DENY', |
506 | 507 | ); |
507 | 508 | foreach ( $wgRestrictionTypes as $type ) { |
508 | 509 | $vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type ); |