From 59506e5abe50b229e7520f2a7ad7415b9b3075c7 Mon Sep 17 00:00:00 2001
From: csteipp <csteipp@wikimedia.org>
Date: Tue, 17 Feb 2015 13:51:29 -0800
Subject: [PATCH] SECURITY: Check limits before parsing XML

To prevent various DoS attacks in our XML parsing, check that PHP has
sane time and memory limits set before parsing when XML entities are
expanded.

Users can set $wgAllowUnsafeLimits = true if they want to use these
features without setting any limits.

Bug: T85848
Change-Id: Ib95edc9c4ad2247f2b58016e8128bcbf9109af6d
---
 includes/DefaultSettings.php            |  7 +++++++
 includes/GlobalFunctions.php            | 15 +++++++++++++++
 includes/Setup.php                      |  7 +++++++
 includes/libs/XmlReaderTypeCheck.php    | 10 ++++++++++
 includes/libs/XmlTypeCheck.php          |  8 ++++++++
 includes/media/SVGMetadataExtractor.php |  8 ++++++++
 includes/media/XMP.php                  |  8 ++++++++
 7 files changed, 63 insertions(+)

diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index f9dc1ed..c38ee7d 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -7404,6 +7404,13 @@ $wgPageLanguageUseDB = false;
 $wgUseLinkNamespaceDBFields = true;
 
 /**
+ * Allow processing user input that could potentially consume significant resources, without
+ * ensuring that sane limits are set by PHP.
+ * @var bool
+ */
+$wgAllowUnsafeLimits = false;
+
+/**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
  * @}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 2025e17..65241a2 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -4198,3 +4198,18 @@ function wfIsConfiguredProxy( $ip ) {
 	wfDeprecated( __METHOD__, '1.24' );
 	return IP::isConfiguredProxy( $ip );
 }
+
+/**
+ * Check resource limits, unless wgAllowUnsafeLimits is set.
+ * @return bool
+ */
+function wfCheckPhpLimits() {
+	global $wgCommandLineMode, $wgAllowUnsafeLimits;
+	if ( !$wgCommandLineMode
+		&& !$wgAllowUnsafeLimits
+		&& ( ini_get( 'memory_limit' ) === '-1' || ini_get( 'max_execution_time' ) === '0' )
+	) {
+		return false;
+	}
+	return true;
+}
diff --git a/includes/Setup.php b/includes/Setup.php
index ed0aabe..6fd4e02 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -675,6 +675,13 @@ foreach ( $wgExtensionFunctions as $func ) {
 wfDebug( "Fully initialised\n" );
 $wgFullyInitialised = true;
 
+if ( !wfCheckPhpLimits() ) {
+	wfWarn( 'Running without memory_limit (currently ' . ini_get( 'memory_limit' ) . ') or ' .
+		'max_execution_time (currently ' . ini_get( 'max_execution_time' ) . ') defined ' .
+		'might allow some DoS attacks to succeed.' );
+}
+
+
 Profiler::instance()->scopedProfileOut( $ps_extensions );
 Profiler::instance()->scopedProfileOut( $ps_setup );
 
diff --git a/includes/libs/XmlReaderTypeCheck.php b/includes/libs/XmlReaderTypeCheck.php
index c66aff5..dddd851 100644
--- a/includes/libs/XmlReaderTypeCheck.php
+++ b/includes/libs/XmlReaderTypeCheck.php
@@ -77,10 +77,20 @@ class XmlReaderTypeCheck {
 	 *        filename (default, true) or if it is a string (false)
 	 * @param array $options list of additional parsing options:
 	 *        processing_instruction_handler: Callback for xml_set_processing_instruction_handler
+	 * @throws MWException
 	 */
 	function __construct( $input, $filterCallback = null, $isFile = true, $options = array() ) {
 		$this->filterCallback = $filterCallback;
 		$this->parserOptions = array_merge( $this->parserOptions, $options );
+
+		// Since we intentionally expand entities, make sure sane resource limits
+		// are setup - T85848
+		if ( !wfCheckPhpLimits() ) {
+			throw new MWException( 'Please set PHP\'s memory_limit and max_execution_time ' .
+				'to safely use this function, or set wgAllowUnsafeLimits to disable '.
+				'this check.' );
+		}
+
 		$this->validateFromInput( $input, $isFile );
 	}
 
diff --git a/includes/libs/XmlTypeCheck.php b/includes/libs/XmlTypeCheck.php
index 0d6c3a6..6d2fecf 100644
--- a/includes/libs/XmlTypeCheck.php
+++ b/includes/libs/XmlTypeCheck.php
@@ -74,11 +74,19 @@ class XmlTypeCheck {
 	 *        filename (default, true) or if it is a string (false)
 	 * @param array $options list of additional parsing options:
 	 *        processing_instruction_handler: Callback for xml_set_processing_instruction_handler
+	 * @throws MWException
 	 */
 	function __construct( $input, $filterCallback = null, $isFile = true, $options = array() ) {
 		$this->filterCallback = $filterCallback;
 		$this->parserOptions = array_merge( $this->parserOptions, $options );
 
+		// If we expands entities, make sure sane resource limits are set - T85848
+		if ( !wfCheckPhpLimits() ) {
+			throw new MWException( 'Please set PHP\'s memory_limit and max_execution_time ' .
+				'to safely use this function, or set wgAllowUnsafeLimits to disable '.
+				'this check.' );
+		}
+
 		if ( $isFile ) {
 			$this->validateFromFile( $input );
 		} else {
diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php
index 2037c33..21152d5 100644
--- a/includes/media/SVGMetadataExtractor.php
+++ b/includes/media/SVGMetadataExtractor.php
@@ -74,6 +74,14 @@ class SVGReader {
 			throw new MWException( "Error getting filesize of SVG." );
 		}
 
+		// Since we intentionally expand entities, make sure sane resource limits
+		// are setup - T85848
+		if ( !wfCheckPhpLimits() ) {
+			throw new MWException( 'Please set PHP\'s memory_limit and max_execution_time ' .
+				'to safely use this function, or set wgAllowUnsafeLimits to disable '.
+				'this check.' );
+		}
+
 		if ( $size > $wgSVGMetadataCutoff ) {
 			$this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." );
 			$contents = file_get_contents( $source, false, null, -1, $wgSVGMetadataCutoff );
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
index 0d341aa..1714731 100644
--- a/includes/media/XMP.php
+++ b/includes/media/XMP.php
@@ -112,6 +112,7 @@ class XMPReader {
 	 * Constructor.
 	 *
 	 * Primary job is to initialize the XMLParser
+	 * @throws MWException
 	 */
 	function __construct() {
 
@@ -120,6 +121,13 @@ class XMPReader {
 			throw new MWException( 'XMP support requires XML Parser' );
 		}
 
+		// If xml_parse expands entities, make sure sane resource limits are set - T85848
+		if ( !wfCheckPhpLimits() ) {
+			throw new MWException( 'Please set PHP\'s memory_limit and max_execution_time ' .
+				'to safely use this function, or set wgAllowUnsafeLimits to disable '.
+				'this check.' );
+		}
+
 		$this->items = XMPInfo::getItems();
 
 		$this->resetXMLParser();
-- 
1.8.4.5

