From e6bb963ff2f44a9626a1e3d0568df045b71c84a1 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:39:16 +0000 Subject: [PATCH 1/4] unified: copy unsafe JavaScript eval query unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../security/CWE-094/UnsafeJsEval.qhelp | 32 ++ .../queries/security/CWE-094/UnsafeJsEval.ql | 25 ++ .../security/CWE-094/UnsafeJsEvalBad.swift | 6 + .../security/CWE-094/UnsafeJsEvalGood.swift | 10 + .../UnsafeJsEval/UnsafeJsEval.expected | 120 +++++++ .../CWE-094/UnsafeJsEval/UnsafeJsEval.qlref | 2 + .../CWE-094/UnsafeJsEval/UnsafeJsEval.swift | 336 ++++++++++++++++++ 7 files changed, 531 insertions(+) create mode 100644 unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp create mode 100644 unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql create mode 100644 unified/ql/src/queries/security/CWE-094/UnsafeJsEvalBad.swift create mode 100644 unified/ql/src/queries/security/CWE-094/UnsafeJsEvalGood.swift create mode 100644 unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected create mode 100644 unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref create mode 100644 unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift 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..a685e1d51d6c --- /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..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) -> 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) // $ 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 $ Alert=source4 + } + testSync { string in + ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentEnd, forMainFrameOnly: true, in: .defaultClient)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + } +} + +func testJSContext() { + let ctx = JSContext() + + testSync { string in + _ = ctx.evaluateScript(string) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + } + testSync { string in + _ = ctx.evaluateScript(string, withSourceURL: URL(string: "https://example.com")) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ 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, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ 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, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ 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() From 9a7bfa035660e942e1222d59322bc575f6654e9d Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:39:57 +0000 Subject: [PATCH 2/4] unified: inline unsafe JavaScript eval configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../queries/security/CWE-094/UnsafeJsEval.ql | 161 +++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql index 8bfdbba02fad..25e0b6cd27d7 100644 --- a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql @@ -14,7 +14,166 @@ import swift import codeql.swift.dataflow.DataFlow -import codeql.swift.security.UnsafeJsEvalQuery +import codeql.swift.dataflow.TaintTracking +import codeql.swift.dataflow.FlowSources +private import codeql.swift.dataflow.ExternalFlow + +/* + * Copied unchanged from + * swift/ql/lib/codeql/swift/security/UnsafeJsEvalQuery.qll. + */ + +/** + * A taint configuration from taint sources to sinks for this query. + */ +module UnsafeJsEvalConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node node) { node instanceof FlowSource } + + predicate isSink(DataFlow::Node node) { node instanceof UnsafeJsEvalSink } + + predicate isBarrier(DataFlow::Node barrier) { barrier instanceof UnsafeJsEvalBarrier } + + predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + any(UnsafeJsEvalAdditionalFlowStep s).step(nodeFrom, nodeTo) + } + + predicate observeDiffInformedIncrementalMode() { any() } +} + +/** + * Detect taint flow of taint sources to sinks for this query. + */ +module UnsafeJsEvalFlow = TaintTracking::Global; + +/* + * Copied unchanged from + * swift/ql/lib/codeql/swift/security/UnsafeJsEvalExtensions.qll. + */ + +/** + * A dataflow sink for javascript evaluation vulnerabilities. + */ +abstract class UnsafeJsEvalSink extends DataFlow::Node { } + +/** + * A barrier for javascript evaluation vulnerabilities. + */ +abstract class UnsafeJsEvalBarrier extends DataFlow::Node { } + +/** + * A unit class for adding additional flow steps. + */ +class UnsafeJsEvalAdditionalFlowStep extends Unit { + /** + * Holds if the step from `node1` to `node2` should be considered a flow + * step for paths related to javascript evaluation vulnerabilities. + */ + abstract predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo); +} + +/** + * A default javascript evaluation sink for the `WKWebView` interface. + */ +private class WKWebViewDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + WKWebViewDefaultUnsafeJsEvalSink() { + any(CallExpr ce | + ce.getStaticTarget() + .(Method) + .hasQualifiedName("WKWebView", + [ + "evaluateJavaScript(_:)", "evaluateJavaScript(_:completionHandler:)", + "evaluateJavaScript(_:in:in:completionHandler:)", + "evaluateJavaScript(_:in:contentWorld:)", + "callAsyncJavaScript(_:arguments:in:in:completionHandler:)", + "callAsyncJavaScript(_:arguments:in:contentWorld:)" + ]) + ).getArgument(0).getExpr() = this.asExpr() + } +} + +/** + * A default javascript evaluation sink for the `WKUserContentController` interface. + */ +private class WKUserContentControllerDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + WKUserContentControllerDefaultUnsafeJsEvalSink() { + any(CallExpr ce | + ce.getStaticTarget().(Method).hasQualifiedName("WKUserContentController", "addUserScript(_:)") + ).getArgument(0).getExpr() = this.asExpr() + } +} + +/** + * A default javascript evaluation sink for the `UIWebView` and `WebView` interfaces. + */ +private class UIWebViewDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + UIWebViewDefaultUnsafeJsEvalSink() { + any(CallExpr ce | + ce.getStaticTarget() + .(Method) + .hasQualifiedName(["UIWebView", "WebView"], "stringByEvaluatingJavaScript(from:)") + ).getArgument(0).getExpr() = this.asExpr() + } +} + +/** + * A default javascript evaluation sink for the `JSContext` interface. + */ +private class JSContextDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + JSContextDefaultUnsafeJsEvalSink() { + any(CallExpr ce | + ce.getStaticTarget() + .(Method) + .hasQualifiedName("JSContext", ["evaluateScript(_:)", "evaluateScript(_:withSourceURL:)"]) + ).getArgument(0).getExpr() = this.asExpr() + } +} + +/** + * A default javascript evaluation sink for the `JSEvaluateScript` function. + */ +private class JSEvaluateScriptDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + JSEvaluateScriptDefaultUnsafeJsEvalSink() { + any(CallExpr ce | ce.getStaticTarget().(FreeFunction).hasName("JSEvaluateScript(_:_:_:_:_:_:)")) + .getArgument(1) + .getExpr() = this.asExpr() + } +} + +/** + * A default javascript evaluation additional taint step. + */ +private class DefaultUnsafeJsEvalAdditionalFlowStep extends UnsafeJsEvalAdditionalFlowStep { + override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + exists(MemberRefExpr e, Expr self, VarDecl member | + self.getType().getFullName().matches(["Unsafe%Buffer%", "Unsafe%Pointer%"]) and + member.getName() = "baseAddress" + | + e.getBase() = self and + e.getMember() = member and + nodeFrom.asExpr() = self and + nodeTo.asExpr() = e + ) + } +} + +/** + * A sink defined in a CSV model. + */ +private class DefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { + DefaultUnsafeJsEvalSink() { sinkNode(this, "code-injection") } +} + +/** + * A barrier for javascript evaluation. + */ +private class UnsafeJsEvalDefaultBarrier extends UnsafeJsEvalBarrier { + UnsafeJsEvalDefaultBarrier() { + // any numeric type + this.asExpr().getType().getUnderlyingType().getABaseType*().getName() = + ["Numeric", "SignedInteger", "UnsignedInteger"] + } +} + import UnsafeJsEvalFlow::PathGraph from From 3ca865e8b1a187333b9e9dd4d82671abde02d97c Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:44:46 +0000 Subject: [PATCH 3/4] unified: port unsafe JavaScript eval query Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- unified/ql/lib/ext/legacy-swift.model.yml | 12 ++ .../queries/security/CWE-094/UnsafeJsEval.ql | 163 ++--------------- .../UnsafeJsEval/UnsafeJsEval.expected | 171 ++++++------------ .../CWE-094/UnsafeJsEval/UnsafeJsEval.qlref | 5 +- .../CWE-094/UnsafeJsEval/UnsafeJsEval.swift | 14 +- 5 files changed, 91 insertions(+), 274 deletions(-) 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.ql b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql index 25e0b6cd27d7..6de65a451d5d 100644 --- a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.ql @@ -5,36 +5,31 @@ * @problem.severity warning * @security-severity 9.3 * @precision medium - * @id swift/unsafe-js-eval + * @id unified/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.dataflow.TaintTracking -import codeql.swift.dataflow.FlowSources -private import codeql.swift.dataflow.ExternalFlow - -/* - * Copied unchanged from - * swift/ql/lib/codeql/swift/security/UnsafeJsEvalQuery.qll. - */ +import unified /** * A taint configuration from taint sources to sinks for this query. */ module UnsafeJsEvalConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node node) { node instanceof FlowSource } + predicate isSource(DataFlow::Node node) { Models::isSource(node, _) } - predicate isSink(DataFlow::Node node) { node instanceof UnsafeJsEvalSink } + predicate isSink(DataFlow::Node node) { Models::isSink(node, "code-injection") } - predicate isBarrier(DataFlow::Node barrier) { barrier instanceof UnsafeJsEvalBarrier } + 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) { - any(UnsafeJsEvalAdditionalFlowStep s).step(nodeFrom, nodeTo) + // TODO: Port the base-address flow step from original Swift's UnsafeJsEvalExtensions.qll. + none() } predicate observeDiffInformedIncrementalMode() { any() } @@ -45,140 +40,8 @@ module UnsafeJsEvalConfig implements DataFlow::ConfigSig { */ module UnsafeJsEvalFlow = TaintTracking::Global; -/* - * Copied unchanged from - * swift/ql/lib/codeql/swift/security/UnsafeJsEvalExtensions.qll. - */ - -/** - * A dataflow sink for javascript evaluation vulnerabilities. - */ -abstract class UnsafeJsEvalSink extends DataFlow::Node { } - -/** - * A barrier for javascript evaluation vulnerabilities. - */ -abstract class UnsafeJsEvalBarrier extends DataFlow::Node { } - -/** - * A unit class for adding additional flow steps. - */ -class UnsafeJsEvalAdditionalFlowStep extends Unit { - /** - * Holds if the step from `node1` to `node2` should be considered a flow - * step for paths related to javascript evaluation vulnerabilities. - */ - abstract predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo); -} - -/** - * A default javascript evaluation sink for the `WKWebView` interface. - */ -private class WKWebViewDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - WKWebViewDefaultUnsafeJsEvalSink() { - any(CallExpr ce | - ce.getStaticTarget() - .(Method) - .hasQualifiedName("WKWebView", - [ - "evaluateJavaScript(_:)", "evaluateJavaScript(_:completionHandler:)", - "evaluateJavaScript(_:in:in:completionHandler:)", - "evaluateJavaScript(_:in:contentWorld:)", - "callAsyncJavaScript(_:arguments:in:in:completionHandler:)", - "callAsyncJavaScript(_:arguments:in:contentWorld:)" - ]) - ).getArgument(0).getExpr() = this.asExpr() - } -} - -/** - * A default javascript evaluation sink for the `WKUserContentController` interface. - */ -private class WKUserContentControllerDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - WKUserContentControllerDefaultUnsafeJsEvalSink() { - any(CallExpr ce | - ce.getStaticTarget().(Method).hasQualifiedName("WKUserContentController", "addUserScript(_:)") - ).getArgument(0).getExpr() = this.asExpr() - } -} - -/** - * A default javascript evaluation sink for the `UIWebView` and `WebView` interfaces. - */ -private class UIWebViewDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - UIWebViewDefaultUnsafeJsEvalSink() { - any(CallExpr ce | - ce.getStaticTarget() - .(Method) - .hasQualifiedName(["UIWebView", "WebView"], "stringByEvaluatingJavaScript(from:)") - ).getArgument(0).getExpr() = this.asExpr() - } -} - -/** - * A default javascript evaluation sink for the `JSContext` interface. - */ -private class JSContextDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - JSContextDefaultUnsafeJsEvalSink() { - any(CallExpr ce | - ce.getStaticTarget() - .(Method) - .hasQualifiedName("JSContext", ["evaluateScript(_:)", "evaluateScript(_:withSourceURL:)"]) - ).getArgument(0).getExpr() = this.asExpr() - } -} - -/** - * A default javascript evaluation sink for the `JSEvaluateScript` function. - */ -private class JSEvaluateScriptDefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - JSEvaluateScriptDefaultUnsafeJsEvalSink() { - any(CallExpr ce | ce.getStaticTarget().(FreeFunction).hasName("JSEvaluateScript(_:_:_:_:_:_:)")) - .getArgument(1) - .getExpr() = this.asExpr() - } -} - -/** - * A default javascript evaluation additional taint step. - */ -private class DefaultUnsafeJsEvalAdditionalFlowStep extends UnsafeJsEvalAdditionalFlowStep { - override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { - exists(MemberRefExpr e, Expr self, VarDecl member | - self.getType().getFullName().matches(["Unsafe%Buffer%", "Unsafe%Pointer%"]) and - member.getName() = "baseAddress" - | - e.getBase() = self and - e.getMember() = member and - nodeFrom.asExpr() = self and - nodeTo.asExpr() = e - ) - } -} - -/** - * A sink defined in a CSV model. - */ -private class DefaultUnsafeJsEvalSink extends UnsafeJsEvalSink { - DefaultUnsafeJsEvalSink() { sinkNode(this, "code-injection") } -} - -/** - * A barrier for javascript evaluation. - */ -private class UnsafeJsEvalDefaultBarrier extends UnsafeJsEvalBarrier { - UnsafeJsEvalDefaultBarrier() { - // any numeric type - this.asExpr().getType().getUnderlyingType().getABaseType*().getName() = - ["Numeric", "SignedInteger", "UnsignedInteger"] - } -} - 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." +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/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected index cfd68d818efc..7d7e2931062f 100644 --- a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.expected @@ -1,120 +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: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 | | +| 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 | call to String.init(contentsOf:) | semmle.label | call to String.init(contentsOf:) | +| 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 | 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 | ... .+(_:_:) ... | +| 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 -#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 index 55cc812d7365..8d2c4ab72b04 100644 --- a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.qlref @@ -1,2 +1,3 @@ -query: queries/Security/CWE-094/UnsafeJsEval.ql -postprocess: utils/test/InlineExpectationsTestQuery.ql +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 index 973b28588578..d82b07229eb4 100644 --- a/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift +++ b/unified/ql/test/query-tests/security/CWE-094/UnsafeJsEval/UnsafeJsEval.swift @@ -208,7 +208,7 @@ func testSync(_ sink: @escaping (String) -> ()) { sink("console.log(" + (try! String(contentsOf: url!)) + ")") // $ Source=source3 let localData = Data(localString.utf8) - let remoteData = Data((try! String(contentsOf: url!)).utf8) // $ Source=source4 + 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)) @@ -263,10 +263,10 @@ func testWKUserContentController() { let ctrl = WKUserContentController() testSync { string in - ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentStart, forMainFrameOnly: false)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + 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 $ Alert=source4 + ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentEnd, forMainFrameOnly: true, in: .defaultClient)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 } } @@ -274,10 +274,10 @@ func testJSContext() { let ctx = JSContext() testSync { string in - _ = ctx.evaluateScript(string) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + _ = 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 $ Alert=source4 + _ = ctx.evaluateScript(string, withSourceURL: URL(string: "https://example.com")) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ MISSING: Alert=source4 } } @@ -288,7 +288,7 @@ func testJSEvaluateScript() { defer { JSStringRelease(jsstr) } _ = JSEvaluateScript( /*ctx:*/ OpaquePointer(bitPattern: 0), - /*script:*/ jsstr, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + /*script:*/ jsstr, // $ MISSING: Alert=source1 MISSING: Alert=source2 MISSING: Alert=source3 MISSING: Alert=source4 /*thisObject:*/ OpaquePointer(bitPattern: 0), /*sourceURL:*/ OpaquePointer(bitPattern: 0), /*startingLineNumber:*/ 0, @@ -302,7 +302,7 @@ func testJSEvaluateScript() { defer { JSStringRelease(jsstr) } _ = JSEvaluateScript( /*ctx:*/ OpaquePointer(bitPattern: 0), - /*script:*/ jsstr, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4 + /*script:*/ jsstr, // $ MISSING: Alert=source1 MISSING: Alert=source2 MISSING: Alert=source3 MISSING: Alert=source4 /*thisObject:*/ OpaquePointer(bitPattern: 0), /*sourceURL:*/ OpaquePointer(bitPattern: 0), /*startingLineNumber:*/ 0, From d41e91bbe5e5b8ed6b51806a6f415134d1056ba1 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:49:41 +0000 Subject: [PATCH 4/4] unified: update unsafe JavaScript eval help link Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp index a685e1d51d6c..f4921ad93b13 100644 --- a/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp +++ b/unified/ql/src/queries/security/CWE-094/UnsafeJsEval.qhelp @@ -25,7 +25,7 @@
  • - Apple Developer Documentation: WKWebView.callAsyncJavaScript(_:arguments:in:contentWorld:) + Apple Developer Documentation: WKWebView.callAsyncJavaScript(_:arguments:in:contentWorld:)