From 997fe0180966f0894288bee97efd25f16d4cc322 Mon Sep 17 00:00:00 2001 From: cpwhidden Date: Tue, 12 Sep 2017 15:20:39 -0500 Subject: [PATCH] [stdlib] Prevent type coercion from Bool to numerical types when decoding JSON and plist JSONEncoder and PropertyListEncoder both use NSNumber to box Bool values. An encoded Bool can be coerced to any numerical type during decoding because (false as NSNumber).intValue == 0. As a remedy, all of the unbox(_:as:) methods of _JSONDecoder and _PlistDecoder for numerical types include a check that the value is not identical to either kCFBooleanTrue or kCFBooleanFalse, and throw a DecodingError._typeMismatch(at:expectation:) if this check fails. --- .../public/SDK/Foundation/JSONEncoder.swift | 24 ++++++------ .../public/SDK/Foundation/PlistEncoder.swift | 24 ++++++------ test/stdlib/TestJSONEncoder.swift | 37 +++++++++++++++++++ test/stdlib/TestPlistEncoder.swift | 37 +++++++++++++++++++ 4 files changed, 98 insertions(+), 24 deletions(-) diff --git a/stdlib/public/SDK/Foundation/JSONEncoder.swift b/stdlib/public/SDK/Foundation/JSONEncoder.swift index c46da27147c75..9901552540bbb 100644 --- a/stdlib/public/SDK/Foundation/JSONEncoder.swift +++ b/stdlib/public/SDK/Foundation/JSONEncoder.swift @@ -1767,7 +1767,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1782,7 +1782,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1797,7 +1797,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1812,7 +1812,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1827,7 +1827,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1842,7 +1842,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1857,7 +1857,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1872,7 +1872,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1887,7 +1887,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1902,7 +1902,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? { guard !(value is NSNull) else { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1917,7 +1917,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? { guard !(value is NSNull) else { return nil } - if let number = value as? NSNumber { + if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { // We are willing to return a Float by losing precision: // * If the original value was integral, // * and the integral value was > Float.greatestFiniteMagnitude, we will fail @@ -1963,7 +1963,7 @@ extension _JSONDecoder { fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? { guard !(value is NSNull) else { return nil } - if let number = value as? NSNumber { + if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { // We are always willing to return the number as a Double: // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double // * If it was a Float or Double, you will get back the precise value diff --git a/stdlib/public/SDK/Foundation/PlistEncoder.swift b/stdlib/public/SDK/Foundation/PlistEncoder.swift index 8bf01e7b337a1..059e2374a558e 100644 --- a/stdlib/public/SDK/Foundation/PlistEncoder.swift +++ b/stdlib/public/SDK/Foundation/PlistEncoder.swift @@ -1549,7 +1549,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1564,7 +1564,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1579,7 +1579,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1594,7 +1594,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1609,7 +1609,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1624,7 +1624,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1639,7 +1639,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1654,7 +1654,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1669,7 +1669,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1684,7 +1684,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1699,7 +1699,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Float.Type) throws -> Float? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } @@ -1714,7 +1714,7 @@ extension _PlistDecoder { fileprivate func unbox(_ value: Any, as type: Double.Type) throws -> Double? { if let string = value as? String, string == _plistNull { return nil } - guard let number = value as? NSNumber else { + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } diff --git a/test/stdlib/TestJSONEncoder.swift b/test/stdlib/TestJSONEncoder.swift index 96a1cd7aab4d6..5ed50b81ffa08 100644 --- a/test/stdlib/TestJSONEncoder.swift +++ b/test/stdlib/TestJSONEncoder.swift @@ -449,6 +449,34 @@ class TestJSONEncoder : TestJSONEncoderSuper { // Optional URLs should encode the same way. _testRoundTrip(of: OptionalTopLevelWrapper(url), expectedJSON: expectedJSON) } + + // MARK: - Type coercion + func testTypeCoercion() { + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) + } // MARK: - Helper Functions private var _jsonEmptyDictionary: Data { @@ -498,6 +526,14 @@ class TestJSONEncoder : TestJSONEncoderSuper { expectUnreachable("Failed to decode \(T.self) from JSON: \(error)") } } + + private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T : Codable, U : Codable { + do { + let data = try JSONEncoder().encode(value) + let _ = try JSONDecoder().decode(U.self, from: data) + expectUnreachable("Coercion from \(T.self) to \(U.self) was expected to fail.") + } catch {} + } } // MARK: - Helper Global Functions @@ -1067,5 +1103,6 @@ JSONEncoderTests.test("testNestedContainerCodingPaths") { TestJSONEncoder().test JSONEncoderTests.test("testSuperEncoderCodingPaths") { TestJSONEncoder().testSuperEncoderCodingPaths() } JSONEncoderTests.test("testInterceptDecimal") { TestJSONEncoder().testInterceptDecimal() } JSONEncoderTests.test("testInterceptURL") { TestJSONEncoder().testInterceptURL() } +JSONEncoderTests.test("testTypeCoercion") { TestJSONEncoder().testTypeCoercion() } runAllTests() #endif diff --git a/test/stdlib/TestPlistEncoder.swift b/test/stdlib/TestPlistEncoder.swift index d62b7a5ef6ee1..d7b037be092c9 100644 --- a/test/stdlib/TestPlistEncoder.swift +++ b/test/stdlib/TestPlistEncoder.swift @@ -170,6 +170,34 @@ class TestPropertyListEncoder : TestPropertyListEncoderSuper { _testRoundTrip(of: topLevel, in: .xml, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)) } + // MARK: - Type coercion + func testTypeCoercion() { + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) + } + // MARK: - Helper Functions private var _plistEmptyDictionaryBinary: Data { return Data(base64Encoded: "YnBsaXN0MDDQCAAAAAAAAAEBAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAJ")! @@ -211,6 +239,14 @@ class TestPropertyListEncoder : TestPropertyListEncoderSuper { expectUnreachable("Failed to decode \(T.self) from plist: \(error)") } } + + private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T : Codable, U : Codable { + do { + let data = try PropertyListEncoder().encode(value) + let _ = try PropertyListDecoder().decode(U.self, from: data) + expectUnreachable("Coercion from \(T.self) to \(U.self) was expected to fail.") + } catch {} + } } // MARK: - Helper Global Functions @@ -694,5 +730,6 @@ PropertyListEncoderTests.test("testSuperEncoderCodingPaths") { TestPropertyListE PropertyListEncoderTests.test("testEncodingTopLevelData") { TestPropertyListEncoder().testEncodingTopLevelData() } PropertyListEncoderTests.test("testInterceptData") { TestPropertyListEncoder().testInterceptData() } PropertyListEncoderTests.test("testInterceptDate") { TestPropertyListEncoder().testInterceptDate() } +PropertyListEncoderTests.test("testTypeCoercion") { TestPropertyListEncoder().testTypeCoercion() } runAllTests() #endif