From e1804b9921cef22aa55f7d2a6482b7e9d0e6dd79 Mon Sep 17 00:00:00 2001 From: Brian Wolff 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( '/(? [ '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