CVE-2017-5887: Improper Certificate Validation (CWE-295)
value: WebSocket.swift in Starscream before 2.0.4 allows an SSL Pinning bypass because pinning occurs in the stream function (this is too late; pinning should occur in the initStreamsWithData function).
CWE description: The product does not validate, or incorrectly validates, a certificate.
CVSS v3 base score: 7.5
Published: 2017-04-06T14:59Z
Repository: https://github.com/daltoniam/Starscream
Language: Swift
Files changed: Source/WebSocket.swift
Commit date: 2017-03-14 13:12:39 -0500
Commit message: some cleanup and SSL pinning fix

Diff: diff --git a/Source/WebSocket.swift b/Source/WebSocket.swift
index 03e5752..3630628 100644
--- a/Source/WebSocket.swift
+++ b/Source/WebSocket.swift
@@ -256,11 +256,10 @@ open class WebSocket : NSObject, StreamDelegate {
 
     /**
      Private method that starts the connection.
      */
     private func createHTTPRequest() {
-
         let urlRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "GET" as CFString,
                                                     url as CFURL, kCFHTTPVersion1_1).takeRetainedValue()
 
         var port = url.port
         if port == nil {
@@ -381,38 +380,42 @@ open class WebSocket : NSObject, StreamDelegate {
             while !outStream.hasSpaceAvailable && !sOperation.isCancelled {
                 usleep(100) // wait until the socket is ready
                 guard !sOperation.isCancelled else { return }
                 out -= 100
                 if out < 0 {
-                    self?.cleanupStream()
+                    WebSocket.sharedWorkQueue.async {
+                        self?.cleanupStream()
+                    }
                     self?.doDisconnect(self?.errorWithDetail("write wait timed out", code: 2))
                     return
                 } else if outStream.streamError != nil {
                     return // disconnectStream will be called.
                 }
             }
-            guard !sOperation.isCancelled else { return }
+            guard !sOperation.isCancelled, let s = self else { return }
+            // Do the pinning now if needed
+            if let sec = s.security, !s.certValidated {
+                let trust = outStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust
+                let domain = outStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String
+                s.certValidated = sec.isValid(trust, domain: domain)
+                if !s.certValidated {
+                    WebSocket.sharedWorkQueue.async {
+                        let error = s.errorWithDetail("Invalid SSL certificate", code: 1)
+                        s.disconnectStream(error)
+                    }
+                    return
+                }
+            }
             outStream.write(bytes, maxLength: data.count)
         }
         writeQueue.addOperation(operation)
     }
     
     /**
      Delegate for the stream methods. Processes incoming bytes
      */
     open func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
-        if let sec = security, !certValidated && [.hasBytesAvailable, .hasSpaceAvailable].contains(eventCode) {
-            let trust = aStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust
-            let domain = aStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String
-            if sec.isValid(trust, domain: domain) {
-                certValidated = true
-            } else {
-                let error = errorWithDetail("Invalid SSL certificate", code: 1)
-                disconnectStream(error)
-                return
-            }
-        }
         if eventCode == .hasBytesAvailable {
             if aStream == inputStream {
                 processInputStream()
             }
         } else if eventCode == .errorOccurred {
@@ -943,10 +946,11 @@ open class WebSocket : NSObject, StreamDelegate {
     deinit {
         mutex.lock()
         readyToWrite = false
         mutex.unlock()
         cleanupStream()
+        writeQueue.cancelAllOperations()
     }
 
 }
 
 private extension Data {
diff --git a/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate b/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate
index 7123ff3..9495132 100644
Binary files a/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate and b/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate differ

Vulnerable code: 
                    self?.cleanupStream()
            guard !sOperation.isCancelled else { return }
        if let sec = security, !certValidated && [.hasBytesAvailable, .hasSpaceAvailable].contains(eventCode) {
            let trust = aStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust
            let domain = aStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String
            if sec.isValid(trust, domain: domain) {
                certValidated = true
            } else {
                let error = errorWithDetail("Invalid SSL certificate", code: 1)
                disconnectStream(error)
                return
            }
        }
Fixed code:                     WebSocket.sharedWorkQueue.async {
                        self?.cleanupStream()
                    }
            guard !sOperation.isCancelled, let s = self else { return }
            // Do the pinning now if needed
            if let sec = s.security, !s.certValidated {
                let trust = outStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust
                let domain = outStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as? String
                s.certValidated = sec.isValid(trust, domain: domain)
                if !s.certValidated {
                    WebSocket.sharedWorkQueue.async {
                        let error = s.errorWithDetail("Invalid SSL certificate", code: 1)
                        s.disconnectStream(error)
                    }
                    return
                }
            }
        writeQueue.cancelAllOperations()