+
+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..8bfdbba02fad
--- /dev/null
+++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql
@@ -0,0 +1,25 @@
+/**
+ * @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 swift/unsafe-js-eval
+ * @tags security
+ * external/cwe/cwe-094
+ * external/cwe/cwe-095
+ * external/cwe/cwe-749
+ */
+
+import swift
+import codeql.swift.dataflow.DataFlow
+import codeql.swift.security.UnsafeJsEvalQuery
+import UnsafeJsEvalFlow::PathGraph
+
+from
+ UnsafeJsEvalFlow::PathNode sourceNode, UnsafeJsEvalFlow::PathNode sinkNode, UnsafeJsEvalSink sink
+where
+ UnsafeJsEvalFlow::flowPath(sourceNode, sinkNode) and
+ sink = sinkNode.getNode()
+select sink, sourceNode, sinkNode, "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..cfd68d818efc
--- /dev/null
+++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected
@@ -0,0 +1,120 @@
+edges
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:265:13:265:13 | string | provenance | |
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:268:13:268:13 | string | provenance | |
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:276:13:276:13 | string | provenance | |
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:279:13:279:13 | string | provenance | |
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:285:13:285:13 | string | provenance | |
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | UnsafeJsEval.swift:299:13:299:13 | string | provenance | |
+| UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:204:7:204:66 | try! ... | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:265:13:265:13 | string | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:268:13:268:13 | string | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:276:13:276:13 | string | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:279:13:279:13 | string | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:285:13:285:13 | string | provenance | |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | UnsafeJsEval.swift:299:13:299:13 | string | provenance | |
+| UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:205:7:205:35 | try! ... | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:265:13:265:13 | string | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:268:13:268:13 | string | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:276:13:276:13 | string | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:279:13:279:13 | string | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:285:13:285:13 | string | provenance | |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | UnsafeJsEval.swift:299:13:299:13 | string | provenance | |
+| UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | provenance | |
+| UnsafeJsEval.swift:211:19:211:60 | call to Data.init(_:) | UnsafeJsEval.swift:214:24:214:24 | remoteData | provenance | |
+| UnsafeJsEval.swift:211:24:211:56 | .utf8 | UnsafeJsEval.swift:211:19:211:60 | call to Data.init(_:) | provenance | |
+| UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:211:24:211:56 | .utf8 | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:265:13:265:13 | string | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:268:13:268:13 | string | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:276:13:276:13 | string | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:279:13:279:13 | string | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:285:13:285:13 | string | provenance | |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | UnsafeJsEval.swift:299:13:299:13 | string | provenance | |
+| UnsafeJsEval.swift:214:24:214:24 | remoteData | UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | provenance | |
+| UnsafeJsEval.swift:265:13:265:13 | string | UnsafeJsEval.swift:266:43:266:43 | string | provenance | |
+| UnsafeJsEval.swift:266:43:266:43 | string | UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | provenance | |
+| UnsafeJsEval.swift:268:13:268:13 | string | UnsafeJsEval.swift:269:43:269:43 | string | provenance | |
+| UnsafeJsEval.swift:269:43:269:43 | string | UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | provenance | |
+| UnsafeJsEval.swift:276:13:276:13 | string | UnsafeJsEval.swift:277:26:277:26 | string | provenance | |
+| UnsafeJsEval.swift:279:13:279:13 | string | UnsafeJsEval.swift:280:26:280:26 | string | provenance | |
+| UnsafeJsEval.swift:285:13:285:13 | string | UnsafeJsEval.swift:286:3:286:10 | .utf16 | provenance | |
+| UnsafeJsEval.swift:286:3:286:10 | .utf16 | UnsafeJsEval.swift:286:51:286:51 | stringBytes [Collection element] | provenance | |
+| UnsafeJsEval.swift:286:51:286:51 | stringBytes [Collection element] | UnsafeJsEval.swift:287:60:287:60 | stringBytes [Collection element] | provenance | |
+| UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) | UnsafeJsEval.swift:291:17:291:17 | jsstr | provenance | |
+| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) | UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) | provenance | |
+| UnsafeJsEval.swift:287:60:287:60 | stringBytes [Collection element] | UnsafeJsEval.swift:287:60:287:72 | .baseAddress | provenance | Config |
+| UnsafeJsEval.swift:287:60:287:72 | .baseAddress | UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) | provenance | |
+| UnsafeJsEval.swift:299:13:299:13 | string | UnsafeJsEval.swift:300:3:300:10 | .utf8CString | provenance | |
+| UnsafeJsEval.swift:300:3:300:10 | .utf8CString | UnsafeJsEval.swift:300:48:300:48 | stringBytes [Collection element] | provenance | |
+| UnsafeJsEval.swift:300:48:300:48 | stringBytes [Collection element] | UnsafeJsEval.swift:301:61:301:61 | stringBytes [Collection element] | provenance | |
+| UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) | UnsafeJsEval.swift:305:17:305:17 | jsstr | provenance | |
+| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) | UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) | provenance | |
+| UnsafeJsEval.swift:301:61:301:61 | stringBytes [Collection element] | UnsafeJsEval.swift:301:61:301:73 | .baseAddress | provenance | Config |
+| UnsafeJsEval.swift:301:61:301:73 | .baseAddress | UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) | provenance | |
+| UnsafeJsEval.swift:318:24:318:87 | call to String.init(contentsOf:) | UnsafeJsEval.swift:320:44:320:74 | ... .+(_:_:) ... | provenance | |
+nodes
+| UnsafeJsEval.swift:204:7:204:66 | try! ... | semmle.label | try! ... |
+| UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) |
+| UnsafeJsEval.swift:205:7:205:35 | try! ... | semmle.label | try! ... |
+| UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) |
+| UnsafeJsEval.swift:208:7:208:58 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... |
+| UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) |
+| UnsafeJsEval.swift:211:19:211:60 | call to Data.init(_:) | semmle.label | call to Data.init(_:) |
+| UnsafeJsEval.swift:211:24:211:56 | .utf8 | semmle.label | .utf8 |
+| UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) |
+| UnsafeJsEval.swift:214:7:214:49 | call to String.init(decoding:as:) | semmle.label | call to String.init(decoding:as:) |
+| UnsafeJsEval.swift:214:24:214:24 | remoteData | semmle.label | remoteData |
+| UnsafeJsEval.swift:265:13:265:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | semmle.label | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) |
+| UnsafeJsEval.swift:266:43:266:43 | string | semmle.label | string |
+| UnsafeJsEval.swift:268:13:268:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | semmle.label | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) |
+| UnsafeJsEval.swift:269:43:269:43 | string | semmle.label | string |
+| UnsafeJsEval.swift:276:13:276:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:277:26:277:26 | string | semmle.label | string |
+| UnsafeJsEval.swift:279:13:279:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:280:26:280:26 | string | semmle.label | string |
+| UnsafeJsEval.swift:285:13:285:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:286:3:286:10 | .utf16 | semmle.label | .utf16 |
+| UnsafeJsEval.swift:286:51:286:51 | stringBytes [Collection element] | semmle.label | stringBytes [Collection element] |
+| UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) | semmle.label | call to JSStringRetain(_:) |
+| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) | semmle.label | call to JSStringCreateWithCharacters(_:_:) |
+| UnsafeJsEval.swift:287:60:287:60 | stringBytes [Collection element] | semmle.label | stringBytes [Collection element] |
+| UnsafeJsEval.swift:287:60:287:72 | .baseAddress | semmle.label | .baseAddress |
+| UnsafeJsEval.swift:291:17:291:17 | jsstr | semmle.label | jsstr |
+| UnsafeJsEval.swift:299:13:299:13 | string | semmle.label | string |
+| UnsafeJsEval.swift:300:3:300:10 | .utf8CString | semmle.label | .utf8CString |
+| UnsafeJsEval.swift:300:48:300:48 | stringBytes [Collection element] | semmle.label | stringBytes [Collection element] |
+| UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) | semmle.label | call to JSStringRetain(_:) |
+| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) | semmle.label | call to JSStringCreateWithUTF8CString(_:) |
+| UnsafeJsEval.swift:301:61:301:61 | stringBytes [Collection element] | semmle.label | stringBytes [Collection element] |
+| UnsafeJsEval.swift:301:61:301:73 | .baseAddress | semmle.label | .baseAddress |
+| UnsafeJsEval.swift:305:17:305:17 | jsstr | semmle.label | jsstr |
+| UnsafeJsEval.swift:318:24:318:87 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) |
+| UnsafeJsEval.swift:320:44:320:74 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... |
+subpaths
+#select
+| UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:266:22:266:107 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:269:22:269:124 | call to WKUserScript.init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:277:26:277:26 | string | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:277:26:277:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:277:26:277:26 | string | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:277:26:277:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:277:26:277:26 | string | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:277:26:277:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:277:26:277:26 | string | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:277:26:277:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:280:26:280:26 | string | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:280:26:280:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:280:26:280:26 | string | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:280:26:280:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:280:26:280:26 | string | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:280:26:280:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:280:26:280:26 | string | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:280:26:280:26 | string | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:291:17:291:17 | jsstr | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:291:17:291:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:291:17:291:17 | jsstr | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:291:17:291:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:291:17:291:17 | jsstr | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:291:17:291:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:291:17:291:17 | jsstr | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:291:17:291:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:305:17:305:17 | jsstr | UnsafeJsEval.swift:204:12:204:66 | call to String.init(contentsOf:) | UnsafeJsEval.swift:305:17:305:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:305:17:305:17 | jsstr | UnsafeJsEval.swift:205:12:205:35 | call to String.init(contentsOf:) | UnsafeJsEval.swift:305:17:305:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:305:17:305:17 | jsstr | UnsafeJsEval.swift:208:30:208:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:305:17:305:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:305:17:305:17 | jsstr | UnsafeJsEval.swift:211:30:211:53 | call to String.init(contentsOf:) | UnsafeJsEval.swift:305:17:305:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
+| UnsafeJsEval.swift:320:44:320:74 | ... .+(_:_:) ... | UnsafeJsEval.swift:318:24:318:87 | call to String.init(contentsOf:) | UnsafeJsEval.swift:320:44:320:74 | ... .+(_:_:) ... | Evaluation of uncontrolled JavaScript from a remote source. |
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..55cc812d7365
--- /dev/null
+++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref
@@ -0,0 +1,2 @@
+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..973b28588578
--- /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