From e1804b9921cef22aa55f7d2a6482b7e9d0e6dd79 Mon Sep 17 00:00:00 2001
From: Brian Wolff <bawolff+wn@gmail.com>
Date: Fri, 13 May 2016 04:27:11 -0400
Subject: [PATCH] SECURITY: Do not allow keys named __proto__ in Json output

Browsers interpret that key name specially when you create just
a plain javascript object. In order to prevent unexepcted behaviour
that in certain cases may be exploitable, ban that key name entirely.

Bug: T134719
Change-Id: Ice111d53d9b3ad164fca3a662e37450e7a2b75d6
---
 includes/json/FormatJson.php                   | 22 +++++++++
 tests/phpunit/includes/json/FormatJsonTest.php | 62 ++++++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php
index 775ab43..2e59cdd 100644
--- a/includes/json/FormatJson.php
+++ b/includes/json/FormatJson.php
@@ -48,6 +48,8 @@ class FormatJson {
 	/**
 	 * Skip escaping as many characters as reasonably possible.
 	 *
+	 * @note This doesn't include PROTO_OK, since banning __proto__ is a different
+	 *  sort of escaping from the character escapes.
 	 * @warning When generating inline script blocks, use FormatJson::UTF8_OK instead.
 	 *
 	 * @since 1.22
@@ -55,6 +57,17 @@ class FormatJson {
 	const ALL_OK = 3;
 
 	/**
+	 * Allow __proto__ as a key name.
+	 *
+	 * @warning Do not use this if having the JSON directly interpreted as Javascript.
+	 *   Web browsers treat __proto__ as containing the object's prototype, which could
+	 *   potentially be used to trick sanitization filters.
+	 *
+	 * @since 1.28
+	 */
+	const PROTO_OK = 4;
+
+	/**
 	 * If set, treat json objects '{...}' as associative arrays. Without this option,
 	 * json objects will be converted to stdClass.
 	 * The value is set to 1 to be backward compatible with 'true' that was used before.
@@ -168,6 +181,15 @@ class FormatJson {
 			$json = str_replace( self::$badChars, self::$badCharsEscaped, $json );
 		}
 
+		if ( !( $escaping & self::PROTO_OK ) ) {
+			if ( preg_match( '/(?<!\\\\)"__proto__":/', $json ) !== 0 ) {
+				throw new UnexpectedValueException(
+					"__proto__ is not allowed as a key name in JSON. "
+					. "(Use self::PROTO_OK to override)"
+				);
+			}
+		}
+
 		return $json;
 	}
 
diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php
index 01b575c..aa91473 100644
--- a/tests/phpunit/includes/json/FormatJsonTest.php
+++ b/tests/phpunit/includes/json/FormatJsonTest.php
@@ -372,4 +372,66 @@ class FormatJsonTest extends MediaWikiTestCase {
 
 		return $cases;
 	}
+
+	public function testProtoOK() {
+		$res = FormatJson::encode(
+			[ '__proto__' => [ 'foo' => 'bar' ] ],
+			false,
+			FormatJson::PROTO_OK
+		);
+		$this->assertEquals( '{"__proto__":{"foo":"bar"}}', $res );
+	}
+
+	/**
+	 * @dataProvider provideProtoNotOkMisses
+	 */
+	public function testProtoNotOkMisses( $expectedUgly, $expectedPretty, $data ) {
+		$actualUgly = FormatJson::encode( $data, false );
+		$actualPretty = FormatJson::encode( $data, true );
+
+		$this->assertEquals( $expectedUgly, $actualUgly, "ugly" );
+		$this->assertEquals( $expectedPretty, $actualPretty, "pretty" );
+	}
+
+	public function provideProtoNotOkMisses() {
+		return [
+			[
+				'{"\"__proto__":{}}',
+				"{\n    \"\\\"__proto__\": {}\n}",
+				[ '"__proto__' => new stdClass ]
+			],
+			[
+				'{"__Proto__":{}}',
+				"{\n    \"__Proto__\": {}\n}",
+				[ '__Proto__' => new stdClass ]
+			],
+			[
+				'{"__PROTO__":{}}',
+				"{\n    \"__PROTO__\": {}\n}",
+				[ '__PROTO__' => new stdClass ]
+			],
+			[
+				'{"foo":"__proto__"}',
+				"{\n    \"foo\": \"__proto__\"\n}",
+				[ 'foo' => "__proto__" ]
+			],
+		];
+	}
+
+	/**
+	 * @expectedException UnexpectedValueException
+	 */
+	public function testProtoNotOkHitUgly() {
+		$data = [ 'foo' => [ 'bar' => 3, '__proto__' => [ 'baz' => 2 ], 'fred' => 'f' ] ];
+		$res = FormatJson::encode( $data, false );
+	}
+
+	/**
+	 * @expectedException UnexpectedValueException
+	 */
+	public function testProtoNotOkHitPretty() {
+		$data = [ 'foo' => [ 'bar' => 3, '__proto__' => [ 'baz' => 2 ], 'fred' => 'f' ] ];
+		$res = FormatJson::encode( $data, true );
+	}
+
 }
-- 
2.0.1

