return;
}
+ # Allow frames by default
+ $wgOut->allowClickjacking();
+
if ( !$wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) {
$parserOptions->setEditSection( false );
}
$sk = $wgUser->getSkin();
$token = $wgUser->editToken( $rcid );
+ $wgOut->preventClickjacking();
$wgOut->addHTML(
"<div class='patrollink'>" .
return;
}
+ $wgOut->preventClickjacking();
+
$tbtext = "";
foreach ( $tbs as $o ) {
$rmvtxt = "";
$wgEnableTooltipsAndAccesskeys = true;
/**
- * Break out of framesets. This can be used to prevent external sites from
- * framing your site with ads.
+ * Break out of framesets. This can be used to prevent clickjacking attacks,
+ * or to prevent external sites from framing your site with ads.
*/
$wgBreakFrames = false;
+/**
+ * The X-Frame-Options header to send on pages sensitive to clickjacking
+ * attacks, such as edit pages. This prevents those pages from being displayed
+ * in a frame or iframe. The options are:
+ *
+ * - 'DENY': Do not allow framing. This is recommended for most wikis.
+ *
+ * - 'SAMEORIGIN': Allow framing by pages on the same domain. This can be used
+ * to allow framing within a trusted domain. This is insecure if there
+ * is a page on the same domain which allows framing of arbitrary URLs.
+ *
+ * - false: Allow all framing. This opens up the wiki to XSS attacks and thus
+ * full compromise of local user accounts. Private wikis behind a
+ * corporate firewall are especially vulnerable. This is not
+ * recommended.
+ *
+ * For extra safety, set $wgBreakFrames = true, to prevent framing on all pages,
+ * not just edit pages.
+ */
+$wgEditPageFrameOptions = 'DENY';
+
/**
* Disable output compression (enabled by default if zlib is available)
*/
function displayForm( $submitResult ) {
global $wgOut;
+ # For good measure (it is the default)
+ $wgOut->preventClickjacking();
+
$html = ''
. $this->getErrors( $submitResult )
. $this->mHeader
$pager->getBody() .
$pager->getNavigationBar()
);
+ $wgOut->preventClickjacking( $pager->getPreventClickjacking() );
wfProfileOut( __METHOD__ );
}
class HistoryPager extends ReverseChronologicalPager {
public $lastRow = false, $counter, $historyPage, $title, $buttons, $conds;
protected $oldIdChecked;
+ protected $preventClickjacking = false;
function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = array() ) {
parent::__construct();
) . "\n";
if ( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $this->preventClickjacking();
$float = $wgContLang->alignEnd();
# Note bug #20966, <button> is non-standard in IE<8
$element = Html::element( 'button',
$this->buttons .= $element;
}
if ( $wgUser->isAllowed( 'revisionmove' ) ) {
+ $this->preventClickjacking();
$float = $wgContLang->alignEnd();
# Note bug #20966, <button> is non-standard in IE<8
$element = Html::element( 'button',
$del = '';
// Show checkboxes for each revision
if ( $wgUser->isAllowed( 'deleterevision' ) || $wgUser->isAllowed( 'revisionmove' ) ) {
+ $this->preventClickjacking();
// If revision was hidden from sysops, disable the checkbox
// However, if the user has revisionmove rights, we cannot disable the checkbox
if ( !$rev->userCan( Revision::DELETED_RESTRICTED ) && !$wgUser->isAllowed( 'revisionmove' ) ) {
# Rollback and undo links
if ( !is_null( $next ) && is_object( $next ) ) {
if ( $latest && $this->title->userCan( 'rollback' ) && $this->title->userCan( 'edit' ) ) {
+ $this->preventClickjacking();
$tools[] = '<span class="mw-rollback-link">' .
$this->getSkin()->buildRollbackLink( $rev ) . '</span>';
}
return '';
}
}
+
+ /**
+ * This is called if a write operation is possible from the generated HTML
+ */
+ function preventClickjacking( $enable = true ) {
+ $this->preventClickjacking = $enable;
+ }
+
+ /**
+ * Get the "prevent clickjacking" flag
+ */
+ function getPreventClickjacking() {
+ return $this->preventClickjacking;
+ }
}
/**
$this->loadFile();
$pager = new ImageHistoryPseudoPager( $this );
$wgOut->addHTML( $pager->getBody() );
+ $wgOut->preventClickjacking( $pager->getPreventClickjacking() );
$this->img->resetHistory(); // free db resources
class ImageHistoryList {
protected $imagePage, $img, $skin, $title, $repo, $showThumb;
+ protected $preventClickjacking = false;
public function __construct( $imagePage ) {
global $wgUser, $wgShowArchiveThumbnails;
# Don't link to unviewable files
$row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>';
} elseif ( $file->isDeleted( File::DELETED_FILE ) ) {
+ $this->preventClickjacking();
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
# Make a link to review the image
$url = $this->skin->link(
return wfMsgHtml( 'filehist-nothumb' );
}
}
+
+ protected function preventClickjacking( $enable = true ) {
+ $this->preventClickjacking = $enable;
+ }
+
+ public function getPreventClickjacking() {
+ return $this->preventClickjacking;
+ }
}
class ImageHistoryPseudoPager extends ReverseChronologicalPager {
+ protected $preventClickjacking = false;
+
function __construct( $imagePage ) {
parent::__construct();
$this->mImagePage = $imagePage;
$s .= $list->imageHistoryLine( !$file->isOld(), $file );
}
$s .= $list->endImageHistoryList( $navLink );
+
+ if ( $list->getPreventClickjacking() ) {
+ $this->preventClickjacking();
+ }
}
return $s;
}
}
$this->mQueryDone = true;
}
+
+ protected function preventClickjacking( $enable = true ) {
+ $this->preventClickjacking = $enable;
+ }
+
+ public function getPreventClickjacking() {
+ return $this->preventClickjacking;
+ }
+
}
var $mPageTitleActionText = '';
var $mParseWarnings = array();
var $mSquidMaxage = 0;
+ var $mPreventClickjacking = true;
var $mRevisionId = null;
protected $mTitle = null;
}
}
+ /**
+ * Set a flag which will cause an X-Frame-Options header appropriate for
+ * edit pages to be sent. The header value is controlled by
+ * $wgEditPageFrameOptions.
+ *
+ * This is the default for special pages. If you display a CSRF-protected
+ * form on an ordinary view page, then you need to call this function.
+ */
+ public function preventClickjacking( $enable = true ) {
+ $this->mPreventClickjacking = $enable;
+ }
+
+ /**
+ * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
+ * This can be called from pages which do not contain any CSRF-protected
+ * HTML form.
+ */
+ public function allowClickjacking() {
+ $this->mPreventClickjacking = false;
+ }
+
+ /**
+ * Get the X-Frame-Options header value (without the name part), or false
+ * if there isn't one. This is used by Skin to determine whether to enable
+ * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
+ */
+ public function getFrameOptions() {
+ global $wgBreakFrames, $wgEditPageFrameOptions;
+ if ( $wgBreakFrames ) {
+ return 'DENY';
+ } elseif ( $this->mPreventClickjacking && $wgEditPageFrameOptions ) {
+ return $wgEditPageFrameOptions;
+ }
+ }
+
/**
* Send cache control HTTP headers
*/
$wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" );
$wgRequest->response()->header( 'Content-language: ' . $wgLanguageCode );
+ // Prevent framing, if requested
+ $frameOptions = $this->getFrameOptions();
+ if ( $frameOptions ) {
+ $wgRequest->response()->header( "X-Frame-Options: $frameOptions" );
+ }
+
if ( $this->mArticleBodyOnly ) {
$this->out( $this->mBodytext );
} else {
'wgUserGroups' => $wgUser->getEffectiveGroups(),
'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0,
'wgCategories' => $wgOut->getCategories(),
+ 'wgBreakFrames' => $wgOut->getFrameOptions() == 'DENY',
);
foreach ( $wgRestrictionTypes as $type ) {
$vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type );
global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
wfProfileIn( __METHOD__ );
+ # Allow frames except in certain special cases
+ $wgOut->allowClickjacking();
# If external diffs are enabled both globally and for the user,
# we'll use the application/x-external-editor interface to call
// Check if page is editable
$editable = $this->mNewRev->getTitle()->userCan( 'edit' );
if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
+ $wgOut->preventClickjacking();
$rollback = '   ' . $sk->generateRollback( $this->mNewRev );
} else {
$rollback = '';
}
// Build the link
if ( $rcid ) {
+ $wgOut->preventClickjacking();
$token = $wgUser->editToken( $rcid );
$patrol = ' <span class="patrollink">[' . $sk->link(
$this->mTitle,
if ( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan( 'patrol' ) ) {
$sk = $wgUser->getSkin();
$token = $wgUser->editToken( $this->mRcidMarkPatrolled );
+ $wgOut->preventClickjacking();
$wgOut->addHTML(
"<div class='patrollink'>[" . $sk->link(
$this->mTitle,
$this->headerDone = true;
$dbTypes = $this->parent->getDBTypes();
- $this->parent->request->response()->header("Content-Type: text/html; charset=utf-8");
+ $this->parent->request->response()->header( 'Content-Type: text/html; charset=utf-8' );
+ $this->parent->request->response()->header( 'X-Frame-Options: DENY' );
if ( $this->redirectTarget ) {
$this->parent->request->response()->header( 'Location: '.$this->redirectTarget );
return;
protected function getConfig( $context ) {
global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
- $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang,
$wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
$wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
$wgSitename, $wgFileExtensions;
'wgServer' => $wgServer,
'wgUserLanguage' => $context->getLanguage(),
'wgContentLanguage' => $wgContLang->getCode(),
- 'wgBreakFrames' => $wgBreakFrames,
'wgVersion' => $wgVersion,
'wgEnableAPI' => $wgEnableAPI,
'wgEnableWriteAPI' => $wgEnableWriteAPI,
$this->setHeaders();
$this->outputHeader();
+ $wgOut->allowClickjacking();
# GET values
$from = $wgRequest->getVal( 'from', null );
$this->setHeaders();
$this->outputHeader();
+ $wgOut->allowClickjacking();
$from = $wgRequest->getText( 'from', $par );
'<p>' . $pager->getNavigationBar() . '</p>'
);
}
+ $wgOut->preventClickjacking( $pager->getPreventClickjacking() );
# Show the appropriate "footer" message - WHOIS tools, etc.
public $mDefaultDirection = true;
var $messages, $target;
var $namespace = '', $mDb;
+ var $preventClickjacking = false;
function __construct( $options ) {
parent::__construct();
if( !$row->page_is_new && $page->quickUserCan( 'rollback' )
&& $page->quickUserCan( 'edit' ) )
{
+ $this->preventClickjacking();
$topmarktext .= ' '.$sk->generateRollback( $rev );
}
}
}
}
+ protected function preventClickjacking() {
+ $this->preventClickjacking = true;
+ }
+
+ public function getPreventClickjacking() {
+ return $this->preventClickjacking;
+ }
}
function execute( $par ) {
global $wgOut, $wgRequest, $wgUrlProtocols, $wgMiserMode, $wgLang;
$this->setHeaders();
+ $wgOut->allowClickjacking();
$target = $wgRequest->getVal( 'target', $par );
$namespace = $wgRequest->getIntorNull( 'namespace', null );
* @param $par String or null
*/
public function execute( $par ) {
- global $wgRequest, $wgUser;
+ global $wgRequest, $wgUser, $wgOut;
$this->setHeaders();
$this->outputHeader();
+ $wgOut->allowClickjacking();
// Strip underscores from title parameter; most of the time we'll want
// text form here. But don't strip underscores from actual text params!
}
function execute( $par ) {
+ global $wgOut;
$this->setHeaders();
$this->outputHeader();
+ $wgOut->allowClickjacking();
$groups = $this->getPageGroups();
$this->setHeaders();
$this->outputHeader();
+ $wgOut->allowClickjacking();
$wgOut->addHTML( Xml::openElement( 'div',
array( 'dir' => $wgContLang->getDir() ) ) );