diff --git a/unified/ql/lib/ext/legacy-swift.model.yml b/unified/ql/lib/ext/legacy-swift.model.yml index d262fadcec1d..4ffd0dac7da6 100644 --- a/unified/ql/lib/ext/legacy-swift.model.yml +++ b/unified/ql/lib/ext/legacy-swift.model.yml @@ -252,6 +252,18 @@ extensions: - ["", "Realm.Configuration", true, "init(deleteRealmIfMigrationNeeded:encryptionKey:fileURL:inMemoryIdentifier:migrationBlock:objectTypes:readOnly:schemaVersion:seedFilePath:shouldCompactOnLaunch:syncConfiguration:)", "", "", "Argument[seedFilePath:]", "path-injection", "manual"] - ["", "Realm.Configuration", true, "fileURL", "", "", "PostUpdate", "path-injection", "manual"] - ["", "Realm.Configuration", true, "seedFilePath", "", "", "PostUpdate", "path-injection", "manual"] + - ["", "WKWebView", true, "evaluateJavaScript(_:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKWebView", true, "evaluateJavaScript(_:completionHandler:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKWebView", true, "evaluateJavaScript(_:in:in:completionHandler:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKWebView", true, "evaluateJavaScript(_:in:contentWorld:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKWebView", true, "callAsyncJavaScript(_:arguments:in:in:completionHandler:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKWebView", true, "callAsyncJavaScript(_:arguments:in:contentWorld:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "WKUserContentController", true, "addUserScript(_:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "UIWebView", true, "stringByEvaluatingJavaScript(from:)", "", "", "Argument[from:]", "code-injection", "manual"] + - ["", "WebView", true, "stringByEvaluatingJavaScript(from:)", "", "", "Argument[from:]", "code-injection", "manual"] + - ["", "JSContext", true, "evaluateScript(_:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "JSContext", true, "evaluateScript(_:withSourceURL:)", "", "", "Argument[0]", "code-injection", "manual"] + - ["", "", false, "JSEvaluateScript(_:_:_:_:_:_:)", "", "", "Argument[1]", "code-injection", "manual"] - ["", "Digest", true, "md5(_:)", "", "", "Argument[0]", "weak-hash-input-MD5", "manual"] - ["", "Digest", true, "sha1(_:)", "", "", "Argument[0]", "weak-hash-input-SHA1", "manual"] - ["", "Digest", true, "sha2(_:variant:)", "", "", "Argument[0]", "weak-password-hash-input-SHA2", "manual"] diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp new file mode 100644 index 000000000000..f4921ad93b13 --- /dev/null +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp @@ -0,0 +1,32 @@ + + + +

Evaluating JavaScript that contains a substring from a remote origin may lead to remote code execution. Code written by an attacker can execute unauthorized actions, including exfiltration of local data through a third party web service.

+ +
+ + +

When loading JavaScript into a web view, evaluate only known, locally-defined source code. If part of the input comes from a remote source, do not inject it into the JavaScript code to be evaluated. Instead, send it to the web view as data using an API such as WKWebView.callAsyncJavaScript with the arguments dictionary to pass remote data objects.

+ +
+ + +

In the following (bad) example, a call to WKWebView.evaluateJavaScript evaluates JavaScript source code that is tainted with remote data, potentially introducing a code injection vulnerability.

+ + + +

In the following (good) example, we sanitize the remote data by passing it using the arguments dictionary of WKWebView.callAsyncJavaScript. This ensures that untrusted data cannot be evaluated as JavaScript source code.

+ + + +
+ + +
  • + Apple Developer Documentation: WKWebView.callAsyncJavaScript(_:arguments:in:contentWorld:) +
  • + +
    +
    diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql new file mode 100644 index 000000000000..6de65a451d5d --- /dev/null +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql @@ -0,0 +1,47 @@ +/** + * @name JavaScript Injection + * @description Evaluating JavaScript code containing a substring from a remote source may lead to remote code execution. + * @kind path-problem + * @problem.severity warning + * @security-severity 9.3 + * @precision medium + * @id unified/swift/unsafe-js-eval + * @tags security + * external/cwe/cwe-094 + * external/cwe/cwe-095 + * external/cwe/cwe-749 + */ + +import unified + +/** + * A taint configuration from taint sources to sinks for this query. + */ +module UnsafeJsEvalConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node node) { Models::isSource(node, _) } + + predicate isSink(DataFlow::Node node) { Models::isSink(node, "code-injection") } + + predicate isBarrier(DataFlow::Node node) { + // TODO: Port the numeric-type barrier from original Swift's UnsafeJsEvalExtensions.qll. + none() + } + + predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + // TODO: Port the base-address flow step from original Swift's UnsafeJsEvalExtensions.qll. + none() + } + + predicate observeDiffInformedIncrementalMode() { any() } +} + +/** + * Detect taint flow of taint sources to sinks for this query. + */ +module UnsafeJsEvalFlow = TaintTracking::Global; + +import UnsafeJsEvalFlow::PathGraph + +from UnsafeJsEvalFlow::PathNode source, UnsafeJsEvalFlow::PathNode sink +where UnsafeJsEvalFlow::flowPath(source, sink) +select sink.getNode(), source, sink, "Evaluation of uncontrolled JavaScript from a remote source." diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalBad.swift b/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalBad.swift new file mode 100644 index 000000000000..111f8100ee29 --- /dev/null +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalBad.swift @@ -0,0 +1,6 @@ +let webview: WKWebView +let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!) + +... + +_ = try await webview.evaluateJavaScript("console.log(" + remoteData + ")") // BAD diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalGood.swift b/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalGood.swift new file mode 100644 index 000000000000..ea8f6c0d5ee1 --- /dev/null +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEvalGood.swift @@ -0,0 +1,10 @@ +let webview: WKWebView +let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!) + +... + +_ = try await webview.callAsyncJavaScript( + "console.log(data)", + arguments: ["data": remoteData], // GOOD + contentWorld: .page +) diff --git a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected new file mode 100644 index 000000000000..7d7e2931062f --- /dev/null +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected @@ -0,0 +1,61 @@ +#select +| UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | UnsafeJsEval.swift:204:12:204:66 | String(...) | UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | UnsafeJsEval.swift:205:12:205:35 | String(...) | UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | UnsafeJsEval.swift:208:30:208:53 | String(...) | UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | UnsafeJsEval.swift:204:12:204:66 | String(...) | UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | UnsafeJsEval.swift:205:12:205:35 | String(...) | UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | UnsafeJsEval.swift:208:30:208:53 | String(...) | UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:277:26:277:31 | string | UnsafeJsEval.swift:204:12:204:66 | String(...) | UnsafeJsEval.swift:277:26:277:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:277:26:277:31 | string | UnsafeJsEval.swift:205:12:205:35 | String(...) | UnsafeJsEval.swift:277:26:277:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:277:26:277:31 | string | UnsafeJsEval.swift:208:30:208:53 | String(...) | UnsafeJsEval.swift:277:26:277:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:280:26:280:31 | string | UnsafeJsEval.swift:204:12:204:66 | String(...) | UnsafeJsEval.swift:280:26:280:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:280:26:280:31 | string | UnsafeJsEval.swift:205:12:205:35 | String(...) | UnsafeJsEval.swift:280:26:280:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:280:26:280:31 | string | UnsafeJsEval.swift:208:30:208:53 | String(...) | UnsafeJsEval.swift:280:26:280:31 | string | Evaluation of uncontrolled JavaScript from a remote source. | +| UnsafeJsEval.swift:320:44:320:76 | ... + ... | UnsafeJsEval.swift:318:24:318:87 | String(...) | UnsafeJsEval.swift:320:44:320:76 | ... + ... | Evaluation of uncontrolled JavaScript from a remote source. | +edges +| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:265:13:265:18 | string | provenance | | +| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:268:13:268:18 | string | provenance | | +| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:276:13:276:18 | string | provenance | | +| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:279:13:279:18 | string | provenance | | +| UnsafeJsEval.swift:204:12:204:66 | String(...) | UnsafeJsEval.swift:204:7:204:66 | try! ... | provenance | | +| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:265:13:265:18 | string | provenance | | +| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:268:13:268:18 | string | provenance | | +| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:276:13:276:18 | string | provenance | | +| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:279:13:279:18 | string | provenance | | +| UnsafeJsEval.swift:205:12:205:35 | String(...) | UnsafeJsEval.swift:205:7:205:35 | try! ... | provenance | | +| UnsafeJsEval.swift:208:7:208:60 | ... + ... | UnsafeJsEval.swift:265:13:265:18 | string | provenance | | +| UnsafeJsEval.swift:208:7:208:60 | ... + ... | UnsafeJsEval.swift:268:13:268:18 | string | provenance | | +| UnsafeJsEval.swift:208:7:208:60 | ... + ... | UnsafeJsEval.swift:276:13:276:18 | string | provenance | | +| UnsafeJsEval.swift:208:7:208:60 | ... + ... | UnsafeJsEval.swift:279:13:279:18 | string | provenance | | +| UnsafeJsEval.swift:208:30:208:53 | String(...) | UnsafeJsEval.swift:208:7:208:60 | ... + ... | provenance | | +| UnsafeJsEval.swift:265:13:265:18 | string | UnsafeJsEval.swift:266:43:266:48 | string | provenance | | +| UnsafeJsEval.swift:266:43:266:48 | string | UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | provenance | | +| UnsafeJsEval.swift:268:13:268:18 | string | UnsafeJsEval.swift:269:43:269:48 | string | provenance | | +| UnsafeJsEval.swift:269:43:269:48 | string | UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | provenance | | +| UnsafeJsEval.swift:276:13:276:18 | string | UnsafeJsEval.swift:277:26:277:31 | string | provenance | | +| UnsafeJsEval.swift:279:13:279:18 | string | UnsafeJsEval.swift:280:26:280:31 | string | provenance | | +| UnsafeJsEval.swift:318:7:318:16 | remoteData | UnsafeJsEval.swift:320:61:320:70 | remoteData | provenance | | +| UnsafeJsEval.swift:318:24:318:87 | String(...) | UnsafeJsEval.swift:318:7:318:16 | remoteData | provenance | | +| UnsafeJsEval.swift:320:61:320:70 | remoteData | UnsafeJsEval.swift:320:44:320:76 | ... + ... | provenance | | +nodes +| UnsafeJsEval.swift:204:7:204:66 | try! ... | semmle.label | try! ... | +| UnsafeJsEval.swift:204:12:204:66 | String(...) | semmle.label | String(...) | +| UnsafeJsEval.swift:205:7:205:35 | try! ... | semmle.label | try! ... | +| UnsafeJsEval.swift:205:12:205:35 | String(...) | semmle.label | String(...) | +| UnsafeJsEval.swift:208:7:208:60 | ... + ... | semmle.label | ... + ... | +| UnsafeJsEval.swift:208:30:208:53 | String(...) | semmle.label | String(...) | +| UnsafeJsEval.swift:265:13:265:18 | string | semmle.label | string | +| UnsafeJsEval.swift:266:22:266:107 | WKUserScript(...) | semmle.label | WKUserScript(...) | +| UnsafeJsEval.swift:266:43:266:48 | string | semmle.label | string | +| UnsafeJsEval.swift:268:13:268:18 | string | semmle.label | string | +| UnsafeJsEval.swift:269:22:269:124 | WKUserScript(...) | semmle.label | WKUserScript(...) | +| UnsafeJsEval.swift:269:43:269:48 | string | semmle.label | string | +| UnsafeJsEval.swift:276:13:276:18 | string | semmle.label | string | +| UnsafeJsEval.swift:277:26:277:31 | string | semmle.label | string | +| UnsafeJsEval.swift:279:13:279:18 | string | semmle.label | string | +| UnsafeJsEval.swift:280:26:280:31 | string | semmle.label | string | +| UnsafeJsEval.swift:318:7:318:16 | remoteData | semmle.label | remoteData | +| UnsafeJsEval.swift:318:24:318:87 | String(...) | semmle.label | String(...) | +| UnsafeJsEval.swift:320:44:320:76 | ... + ... | semmle.label | ... + ... | +| UnsafeJsEval.swift:320:61:320:70 | remoteData | semmle.label | remoteData | +subpaths diff --git a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref new file mode 100644 index 000000000000..8d2c4ab72b04 --- /dev/null +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref @@ -0,0 +1,3 @@ +query: queries/security/CWE-094/UnsafeJsEval.ql +postprocess: + - utils/test/InlineExpectationsTestQuery.ql diff --git a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift new file mode 100644 index 000000000000..d82b07229eb4 --- /dev/null +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift @@ -0,0 +1,336 @@ + +// --- stubs --- + +class NSObject {} + +@MainActor class UIResponder : NSObject {} +@MainActor class UIView : UIResponder {} + +@MainActor class NSResponder : NSObject {} +class NSView : NSResponder {} + +class WKFrameInfo : NSObject {} +class WKContentWorld : NSObject { + class var defaultClient: WKContentWorld { WKContentWorld() } + class var page: WKContentWorld { WKContentWorld() } +} + +class WKWebView : UIView { + + func evaluateJavaScript( + _ javaScriptString: String + ) async throws -> Any { "" } + + func evaluateJavaScript( + _ javaScriptString: String, + completionHandler: ((Any?, Error?) -> Void)? = nil + ) { + completionHandler?(nil, nil) + } + + @MainActor func evaluateJavaScript( + _ javaScript: String, + in frame: WKFrameInfo? = nil, + in contentWorld: WKContentWorld, + completionHandler: ((Result) -> Void)? = nil + ) { + completionHandler?(.success("")) + } + + @MainActor func evaluateJavaScript( + _ javaScript: String, + in frame: WKFrameInfo? = nil, + contentWorld: WKContentWorld + ) async throws -> Any? { nil } + + @MainActor func callAsyncJavaScript( + _ functionBody: String, + arguments: [String : Any] = [:], + in frame: WKFrameInfo? = nil, + in contentWorld: WKContentWorld, + completionHandler: ((Result) -> Void)? = nil + ) { + completionHandler?(.success("")) + } + + @MainActor func callAsyncJavaScript( + _ functionBody: String, + arguments: [String : Any] = [:], + in frame: WKFrameInfo? = nil, + contentWorld: WKContentWorld + ) async throws -> Any? { nil } +} + +enum WKUserScriptInjectionTime : Int, @unchecked Sendable { + case atDocumentStart, atDocumentEnd +} + +class WKUserScript : NSObject { + init( + source: String, + injectionTime: WKUserScriptInjectionTime, + forMainFrameOnly: Bool + ) {} + + init( + source: String, + injectionTime: WKUserScriptInjectionTime, + forMainFrameOnly: Bool, + in contentWorld: WKContentWorld + ) {} +} + +class WKUserContentController : NSObject { + func addUserScript(_ userScript: WKUserScript) {} +} + +class UIWebView : UIView { + // deprecated + func stringByEvaluatingJavaScript(from script: String) -> String? { nil } +} + +class WebView : NSView { + // deprecated + func stringByEvaluatingJavaScript(from script: String!) -> String! { "" } +} + +class JSValue : NSObject {} + +class JSContext { + func evaluateScript(_ script: String!) -> JSValue! { return JSValue() } + func evaluateScript( + _ script: String!, + withSourceURL sourceURL: URL! + ) -> JSValue! { return JSValue() } +} + +typealias JSContextRef = OpaquePointer +typealias JSStringRef = OpaquePointer +typealias JSObjectRef = OpaquePointer +typealias JSValueRef = OpaquePointer +typealias JSChar = UInt16 + +func JSStringCreateWithCharacters( + _ chars: UnsafePointer!, + _ numChars: Int +) -> JSStringRef! { + return chars.withMemoryRebound(to: CChar.self, capacity: numChars) { + cchars in OpaquePointer(cchars) + } +} +func JSStringCreateWithUTF8CString(_ string: UnsafePointer!) -> JSStringRef! { + return OpaquePointer(string) +} +func JSStringRetain(_ string: JSStringRef!) -> JSStringRef! { return string } +func JSStringRelease(_ string: JSStringRef!) { } + +func JSEvaluateScript( + _ ctx: JSContextRef!, + _ script: JSStringRef!, + _ thisObject: JSObjectRef!, + _ sourceURL: JSStringRef!, + _ startingLineNumber: Int32, + _ exception: UnsafeMutablePointer! +) -> JSValueRef! { return OpaquePointer(bitPattern: 0) } + +@frozen +public struct Data: Collection { + public typealias Index = Int + public typealias Element = UInt8 + public subscript(x: Index) -> Element { 0 } + public var startIndex: Index { 0 } + public var endIndex: Index { 0 } + public func index(after i: Index) -> Index { i + 1 } + init(_ elements: S) {} +} + +struct URL { + init?(string: String) {} + init?(string: String, relativeTo: URL?) {} +} + +extension String { + init(contentsOf: URL) throws { + let data = "" + // ... + self.init(data) + } +} + +// --- tests --- + + + + + + + + + + +func testAsync(_ sink: @escaping (String) async throws -> ()) { + Task { + let localString = "console.log('localString')" + let localStringFragment = "'localStringFragment'" + let url = URL(string: "http://example.com/") + + try! await sink(localString) // GOOD: the HTML data is local + try! await sink(try String(contentsOf: URL(string: "http://example.com/")!)) // $ MISSING: Alert (HTML contains remote input, may access local secrets) + try! await sink(try! String(contentsOf: url!)) // $ MISSING: Alert + + try! await sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local + try! await sink("console.log(" + (try! String(contentsOf: url!)) + ")") // $ MISSING: Alert + + let localData = Data(localString.utf8) + let remoteData = Data((try! String(contentsOf: url!)).utf8) + + try! await sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local + try! await sink(String(decoding: remoteData, as: UTF8.self)) // $ MISSING: Alert the data is remote + + try! await sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion + try! await sink("console.log(" + String(Int(try! String(contentsOf: url!)) ?? 0) + ")") // GOOD: Primitive conversion + + try! await sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion + try! await sink("console.log(" + ((try! String(contentsOf: url!)).count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion + } +} + +func testSync(_ sink: @escaping (String) -> ()) { + let localString = "console.log('localString')" + let localStringFragment = "'localStringFragment'" + let url = URL(string: "http://example.com/") + + sink(localString) // GOOD: the HTML data is local + sink(try! String(contentsOf: URL(string: "http://example.com/")!)) // $ Source=source1 + sink(try! String(contentsOf: url!)) // $ Source=source2 + + sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local + sink("console.log(" + (try! String(contentsOf: url!)) + ")") // $ Source=source3 + + let localData = Data(localString.utf8) + let remoteData = Data((try! String(contentsOf: url!)).utf8) // $ MISSING: Source=source4 + + sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local + sink(String(decoding: remoteData, as: UTF8.self)) + + sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion + sink("console.log(" + String(Int(try! String(contentsOf: url!)) ?? 0) + ")") // GOOD: Primitive conversion + + sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion + sink("console.log(" + ((try! String(contentsOf: url!)).count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion +} + +func testUIWebView() { + let webview = UIWebView() + + testAsync { string in + _ = await webview.stringByEvaluatingJavaScript(from: string) // $ MISSING: Alert + } +} + +func testWebView() { + let webview = WebView() + + testAsync { string in + _ = await webview.stringByEvaluatingJavaScript(from: string) // $ MISSING: Alert + } +} + +func testWKWebView() { + let webview = WKWebView() + + testAsync { string in + _ = try await webview.evaluateJavaScript(string) // $ MISSING: Alert + } + testAsync { string in + await webview.evaluateJavaScript(string) { _, _ in } // $ MISSING: Alert + } + testAsync { string in + await webview.evaluateJavaScript(string, in: nil, in: WKContentWorld.defaultClient) { _ in } // $ MISSING: Alert + } + testAsync { string in + _ = try await webview.evaluateJavaScript(string, contentWorld: .defaultClient) // $ MISSING: Alert + } + testAsync { string in + await webview.callAsyncJavaScript(string, in: nil, in: .defaultClient) { _ in () } // $ MISSING: Alert + } + testAsync { string in + _ = try await webview.callAsyncJavaScript(string, contentWorld: WKContentWorld.defaultClient) // $ MISSING: Alert + } +} + +func testWKUserContentController() { + let ctrl = WKUserContentController() + + testSync { string in + ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentStart, forMainFrameOnly: false)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 + } + testSync { string in + ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentEnd, forMainFrameOnly: true, in: .defaultClient)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 + } +} + +func testJSContext() { + let ctx = JSContext() + + testSync { string in + _ = ctx.evaluateScript(string) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 + } + testSync { string in + _ = ctx.evaluateScript(string, withSourceURL: URL(string: "https://example.com")) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 + } +} + +func testJSEvaluateScript() { + testSync { string in + string.utf16.withContiguousStorageIfAvailable { stringBytes in + let jsstr = JSStringRetain(JSStringCreateWithCharacters(stringBytes.baseAddress, string.count)) + defer { JSStringRelease(jsstr) } + _ = JSEvaluateScript( + /*ctx:*/ OpaquePointer(bitPattern: 0), + /*script:*/ jsstr, // $ MISSING: Alert=source1 MISSING: Alert=source2 MISSING: Alert=source3 MISSING: Alert=source4 + /*thisObject:*/ OpaquePointer(bitPattern: 0), + /*sourceURL:*/ OpaquePointer(bitPattern: 0), + /*startingLineNumber:*/ 0, + /*exception:*/ UnsafeMutablePointer(bitPattern: 0) + ) + } + } + testSync { string in + string.utf8CString.withUnsafeBufferPointer { stringBytes in + let jsstr = JSStringRetain(JSStringCreateWithUTF8CString(stringBytes.baseAddress)) + defer { JSStringRelease(jsstr) } + _ = JSEvaluateScript( + /*ctx:*/ OpaquePointer(bitPattern: 0), + /*script:*/ jsstr, // $ MISSING: Alert=source1 MISSING: Alert=source2 MISSING: Alert=source3 MISSING: Alert=source4 + /*thisObject:*/ OpaquePointer(bitPattern: 0), + /*sourceURL:*/ OpaquePointer(bitPattern: 0), + /*startingLineNumber:*/ 0, + /*exception:*/ UnsafeMutablePointer(bitPattern: 0) + ) + } + } +} + +func testQHelpExamples() { + Task { + let webview = WKWebView() + let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!) // $ Source=source5 + + _ = try await webview.evaluateJavaScript("console.log(" + remoteData + ")") // $ Alert=source5 + + _ = try await webview.callAsyncJavaScript( + "console.log(data)", + arguments: ["data": remoteData], // GOOD + contentWorld: .page + ) + } +} + +testUIWebView() +testWebView() +testWKWebView() +testWKUserContentController() +testJSContext() +testJSEvaluateScript() +testQHelpExamples()