mirror of
https://github.com/github/codeql.git
synced 2026-04-20 06:24:03 +02:00
Merge pull request #11001 from d10c/swift/js-injection
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"omnisharp.autoStart": false
|
||||
"omnisharp.autoStart": false,
|
||||
"cmake.sourceDirectory": "${workspaceFolder}/swift",
|
||||
"cmake.buildDirectory": "${workspaceFolder}/bazel-cmake-build"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
|
||||
if(APPLE)
|
||||
set(CMAKE_OSX_ARCHITECTURES x86_64) # temporary until we can build a Universal Binary
|
||||
endif()
|
||||
|
||||
project(codeql)
|
||||
|
||||
include(../misc/bazel/cmake/setup.cmake)
|
||||
|
||||
@@ -1015,6 +1015,14 @@ module Decls {
|
||||
|
||||
module Exprs {
|
||||
module AssignExprs {
|
||||
/**
|
||||
* The control-flow of a `DiscardAssignmentExpr`, which represents the
|
||||
* `_` leaf expression that may appear on the left-hand side of an `AssignExpr`.
|
||||
*/
|
||||
private class DiscardAssignmentExprTree extends AstLeafTree {
|
||||
override DiscardAssignmentExpr ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* The control-flow of an assignment operation.
|
||||
*
|
||||
|
||||
32
swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
Normal file
32
swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>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.</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>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 <code>WKWebView.callAsyncJavaScript</code> with the <code>arguments</code> dictionary to pass remote data objects.</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>In the following (bad) example, a call to <code>WKWebView.evaluateJavaScript</code> evaluates JavaScript source code that is tainted with remote data, potentially introducing a code injection vulnerability.</p>
|
||||
|
||||
<sample src="UnsafeJsEvalBad.swift" />
|
||||
|
||||
<p>In the following (good) example, we sanitize the remote data by passing it using the <code>arguments</code> dictionary of <code>WKWebView.callAsyncJavaScript</code>. This ensures that untrusted data cannot be evaluated as JavaScript source code.</p>
|
||||
|
||||
<sample src="UnsafeJsEvalGood.swift" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
<li>
|
||||
Apple Developer Documentation: <a href="https://developer.apple.com/documentation/webkit/wkwebview/3824703-callasyncjavascript">WKWebView.callAsyncJavaScript(_:arguments:in:contentWorld:)</a>
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
165
swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Normal file
165
swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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 high
|
||||
* @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.dataflow.TaintTracking
|
||||
import codeql.swift.dataflow.FlowSources
|
||||
import DataFlow::PathGraph
|
||||
|
||||
/**
|
||||
* A source of untrusted, user-controlled data.
|
||||
* TODO: Extend to more (non-remote) sources in the future.
|
||||
*/
|
||||
class Source = RemoteFlowSource;
|
||||
|
||||
/**
|
||||
* A sink that evaluates a string of JavaScript code.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
class WKWebView extends Sink {
|
||||
WKWebView() {
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(MethodDecl)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
class WKUserContentController extends Sink {
|
||||
WKUserContentController() {
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(MethodDecl)
|
||||
.hasQualifiedName("WKUserContentController", "addUserScript(_:)")
|
||||
).getArgument(0).getExpr() = this.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
class UIWebView extends Sink {
|
||||
UIWebView() {
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(MethodDecl)
|
||||
.hasQualifiedName(["UIWebView", "WebView"], "stringByEvaluatingJavaScript(from:)")
|
||||
).getArgument(0).getExpr() = this.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
class JSContext extends Sink {
|
||||
JSContext() {
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(MethodDecl)
|
||||
.hasQualifiedName("JSContext", ["evaluateScript(_:)", "evaluateScript(_:withSourceURL:)"])
|
||||
).getArgument(0).getExpr() = this.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
class JSEvaluateScript extends Sink {
|
||||
JSEvaluateScript() {
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget().(FreeFunctionDecl).hasName("JSEvaluateScript(_:_:_:_:_:_:)")
|
||||
).getArgument(1).getExpr() = this.asExpr()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint configuration from taint sources to sinks for this query.
|
||||
*/
|
||||
class UnsafeJsEvalConfig extends TaintTracking::Configuration {
|
||||
UnsafeJsEvalConfig() { this = "UnsafeJsEvalConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node node) { node instanceof Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node node) { node instanceof Sink }
|
||||
|
||||
// TODO: convert to new taint flow models
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(Argument arg |
|
||||
arg =
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(MethodDecl)
|
||||
.hasQualifiedName("WKUserScript",
|
||||
[
|
||||
"init(source:injectionTime:forMainFrameOnly:)",
|
||||
"init(source:injectionTime:forMainFrameOnly:in:)"
|
||||
])
|
||||
).getArgument(0)
|
||||
or
|
||||
arg =
|
||||
any(CallExpr ce | ce.getStaticTarget().(MethodDecl).hasQualifiedName("Data", "init(_:)"))
|
||||
.getArgument(0)
|
||||
or
|
||||
arg =
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget().(MethodDecl).hasQualifiedName("String", "init(decoding:as:)")
|
||||
).getArgument(0)
|
||||
or
|
||||
arg =
|
||||
any(CallExpr ce |
|
||||
ce.getStaticTarget()
|
||||
.(FreeFunctionDecl)
|
||||
.hasName([
|
||||
"JSStringCreateWithUTF8CString(_:)", "JSStringCreateWithCharacters(_:_:)",
|
||||
"JSStringRetain(_:)"
|
||||
])
|
||||
).getArgument(0)
|
||||
|
|
||||
nodeFrom.asExpr() = arg.getExpr() and
|
||||
nodeTo.asExpr() = arg.getApplyExpr()
|
||||
)
|
||||
or
|
||||
exists(CallExpr ce, Expr self, AbstractClosureExpr closure |
|
||||
ce.getStaticTarget()
|
||||
.getName()
|
||||
.matches(["withContiguousStorageIfAvailable(%)", "withUnsafeBufferPointer(%)"]) and
|
||||
self = ce.getQualifier() and
|
||||
ce.getArgument(0).getExpr() = closure
|
||||
|
|
||||
nodeFrom.asExpr() = self and
|
||||
nodeTo.(DataFlow::ParameterNode).getParameter() = closure.getParam(0)
|
||||
)
|
||||
or
|
||||
exists(MemberRefExpr e, Expr self, VarDecl member |
|
||||
self.getType().getName() = "String" and
|
||||
member.getName() = ["utf8", "utf16", "utf8CString"]
|
||||
or
|
||||
self.getType().getName().matches(["Unsafe%Buffer%", "Unsafe%Pointer%"]) and
|
||||
member.getName() = "baseAddress"
|
||||
|
|
||||
e.getBase() = self and
|
||||
e.getMember() = member and
|
||||
nodeFrom.asExpr() = self and
|
||||
nodeTo.asExpr() = e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
from
|
||||
UnsafeJsEvalConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode, Sink sink
|
||||
where
|
||||
config.hasFlowPath(sourceNode, sinkNode) and
|
||||
sink = sinkNode.getNode()
|
||||
select sink, sourceNode, sinkNode, "Evaluation of uncontrolled JavaScript from a remote source."
|
||||
@@ -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
|
||||
10
swift/ql/src/queries/Security/CWE-094/UnsafeJsEvalGood.swift
Normal file
10
swift/ql/src/queries/Security/CWE-094/UnsafeJsEvalGood.swift
Normal file
@@ -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
|
||||
)
|
||||
110
swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.expected
Normal file
110
swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.expected
Normal file
@@ -0,0 +1,110 @@
|
||||
edges
|
||||
| UnsafeJsEval.swift:124:21:124:42 | string : | UnsafeJsEval.swift:124:70:124:70 | string : |
|
||||
| UnsafeJsEval.swift:144:5:144:29 | [summary param] 0 in init(_:) : | file://:0:0:0:0 | [summary] to write: return (return) in init(_:) : |
|
||||
| UnsafeJsEval.swift:165:10:165:37 | try ... : | UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : |
|
||||
| UnsafeJsEval.swift:165:14:165:37 | call to init(contentsOf:) : | UnsafeJsEval.swift:165:10:165:37 | try ... : |
|
||||
| UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : | UnsafeJsEval.swift:205:7:205:7 | remoteString : |
|
||||
| UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : | UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : |
|
||||
| UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : | UnsafeJsEval.swift:211:24:211:37 | .utf8 : |
|
||||
| UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : | UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:265:13:265:13 | string : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:268:13:268:13 | string : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:276:13:276:13 | string : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:279:13:279:13 | string : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:285:13:285:13 | string : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | UnsafeJsEval.swift:299:13:299:13 | string : |
|
||||
| UnsafeJsEval.swift:204:12:204:66 | call to init(contentsOf:) : | UnsafeJsEval.swift:204:7:204:66 | try! ... : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:265:13:265:13 | string : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:268:13:268:13 | string : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:276:13:276:13 | string : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:279:13:279:13 | string : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:285:13:285:13 | string : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | UnsafeJsEval.swift:299:13:299:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:265:13:265:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:268:13:268:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:276:13:276:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:279:13:279:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:285:13:285:13 | string : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | UnsafeJsEval.swift:299:13:299:13 | string : |
|
||||
| UnsafeJsEval.swift:211:19:211:41 | call to init(_:) : | UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : |
|
||||
| UnsafeJsEval.swift:211:24:211:37 | .utf8 : | UnsafeJsEval.swift:144:5:144:29 | [summary param] 0 in init(_:) : |
|
||||
| UnsafeJsEval.swift:211:24:211:37 | .utf8 : | UnsafeJsEval.swift:211:19:211:41 | call to init(_:) : |
|
||||
| UnsafeJsEval.swift:211:24:211:37 | .utf8 : | UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:265:13:265:13 | string : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:268:13:268:13 | string : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:276:13:276:13 | string : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:279:13:279:13 | string : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:285:13:285:13 | string : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | UnsafeJsEval.swift:299:13:299:13 | string : |
|
||||
| UnsafeJsEval.swift:265:13:265:13 | string : | UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) |
|
||||
| UnsafeJsEval.swift:268:13:268:13 | string : | UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) |
|
||||
| UnsafeJsEval.swift:276:13:276:13 | string : | UnsafeJsEval.swift:277:26:277:26 | string |
|
||||
| UnsafeJsEval.swift:279:13:279:13 | string : | UnsafeJsEval.swift:280:26:280:26 | string |
|
||||
| UnsafeJsEval.swift:285:13:285:13 | string : | UnsafeJsEval.swift:286:3:286:10 | .utf16 : |
|
||||
| UnsafeJsEval.swift:286:3:286:10 | .utf16 : | UnsafeJsEval.swift:286:51:286:51 | stringBytes : |
|
||||
| UnsafeJsEval.swift:286:51:286:51 | stringBytes : | UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : |
|
||||
| UnsafeJsEval.swift:286:51:286:51 | stringBytes : | UnsafeJsEval.swift:291:17:291:17 | jsstr |
|
||||
| UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) : | UnsafeJsEval.swift:291:17:291:17 | jsstr |
|
||||
| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : | UnsafeJsEval.swift:124:21:124:42 | string : |
|
||||
| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : | UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) : |
|
||||
| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : | UnsafeJsEval.swift:291:17:291:17 | jsstr |
|
||||
| UnsafeJsEval.swift:299:13:299:13 | string : | UnsafeJsEval.swift:300:3:300:10 | .utf8CString : |
|
||||
| UnsafeJsEval.swift:300:3:300:10 | .utf8CString : | UnsafeJsEval.swift:300:48:300:48 | stringBytes : |
|
||||
| UnsafeJsEval.swift:300:48:300:48 | stringBytes : | UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) : |
|
||||
| UnsafeJsEval.swift:300:48:300:48 | stringBytes : | UnsafeJsEval.swift:305:17:305:17 | jsstr |
|
||||
| UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) : | UnsafeJsEval.swift:305:17:305:17 | jsstr |
|
||||
| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) : | UnsafeJsEval.swift:124:21:124:42 | string : |
|
||||
| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) : | UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) : |
|
||||
| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) : | UnsafeJsEval.swift:305:17:305:17 | jsstr |
|
||||
nodes
|
||||
| UnsafeJsEval.swift:124:21:124:42 | string : | semmle.label | string : |
|
||||
| UnsafeJsEval.swift:124:70:124:70 | string : | semmle.label | string : |
|
||||
| UnsafeJsEval.swift:144:5:144:29 | [summary param] 0 in init(_:) : | semmle.label | [summary param] 0 in init(_:) : |
|
||||
| UnsafeJsEval.swift:165:10:165:37 | try ... : | semmle.label | try ... : |
|
||||
| UnsafeJsEval.swift:165:14:165:37 | call to init(contentsOf:) : | semmle.label | call to init(contentsOf:) : |
|
||||
| UnsafeJsEval.swift:201:21:201:35 | call to getRemoteData() : | semmle.label | call to getRemoteData() : |
|
||||
| UnsafeJsEval.swift:204:7:204:66 | try! ... : | semmle.label | try! ... : |
|
||||
| UnsafeJsEval.swift:204:12:204:66 | call to init(contentsOf:) : | semmle.label | call to init(contentsOf:) : |
|
||||
| UnsafeJsEval.swift:205:7:205:7 | remoteString : | semmle.label | remoteString : |
|
||||
| UnsafeJsEval.swift:208:7:208:39 | ... .+(_:_:) ... : | semmle.label | ... .+(_:_:) ... : |
|
||||
| UnsafeJsEval.swift:211:19:211:41 | call to init(_:) : | semmle.label | call to init(_:) : |
|
||||
| UnsafeJsEval.swift:211:24:211:37 | .utf8 : | semmle.label | .utf8 : |
|
||||
| UnsafeJsEval.swift:214:7:214:49 | call to init(decoding:as:) : | semmle.label | call to init(decoding:as:) : |
|
||||
| UnsafeJsEval.swift:265:13:265:13 | string : | semmle.label | string : |
|
||||
| UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) | semmle.label | call to init(source:injectionTime:forMainFrameOnly:) |
|
||||
| UnsafeJsEval.swift:268:13:268:13 | string : | semmle.label | string : |
|
||||
| UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) | semmle.label | call to init(source:injectionTime:forMainFrameOnly:in:) |
|
||||
| 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 : | semmle.label | stringBytes : |
|
||||
| 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: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 : | semmle.label | stringBytes : |
|
||||
| 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:305:17:305:17 | jsstr | semmle.label | jsstr |
|
||||
| file://:0:0:0:0 | [summary] to write: return (return) in init(_:) : | semmle.label | [summary] to write: return (return) in init(_:) : |
|
||||
subpaths
|
||||
| UnsafeJsEval.swift:211:24:211:37 | .utf8 : | UnsafeJsEval.swift:144:5:144:29 | [summary param] 0 in init(_:) : | file://:0:0:0:0 | [summary] to write: return (return) in init(_:) : | UnsafeJsEval.swift:211:19:211:41 | call to init(_:) : |
|
||||
| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : | UnsafeJsEval.swift:124:21:124:42 | string : | UnsafeJsEval.swift:124:70:124:70 | string : | UnsafeJsEval.swift:287:16:287:98 | call to JSStringRetain(_:) : |
|
||||
| UnsafeJsEval.swift:301:31:301:84 | call to JSStringCreateWithUTF8CString(_:) : | UnsafeJsEval.swift:124:21:124:42 | string : | UnsafeJsEval.swift:124:70:124:70 | string : | UnsafeJsEval.swift:301:16:301:85 | call to JSStringRetain(_:) : |
|
||||
#select
|
||||
| UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:165:14:165:37 | call to init(contentsOf:) : | UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
|
||||
| UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) | UnsafeJsEval.swift:204:12:204:66 | call to init(contentsOf:) : | UnsafeJsEval.swift:266:22:266:107 | call to init(source:injectionTime:forMainFrameOnly:) | Evaluation of uncontrolled JavaScript from a remote source. |
|
||||
| UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:165:14:165:37 | call to init(contentsOf:) : | UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
|
||||
| UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) | UnsafeJsEval.swift:204:12:204:66 | call to init(contentsOf:) : | UnsafeJsEval.swift:269:22:269:124 | call to init(source:injectionTime:forMainFrameOnly:in:) | Evaluation of uncontrolled JavaScript from a remote source. |
|
||||
| UnsafeJsEval.swift:277:26:277:26 | string | UnsafeJsEval.swift:165:14:165:37 | call to 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:204:12:204:66 | call to 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:165:14:165:37 | call to 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:204:12:204:66 | call to 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:165:14:165:37 | call to 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:204:12:204:66 | call to 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:165:14:165:37 | call to 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:204:12:204:66 | call to init(contentsOf:) : | UnsafeJsEval.swift:305:17:305:17 | jsstr | Evaluation of uncontrolled JavaScript from a remote source. |
|
||||
@@ -0,0 +1 @@
|
||||
queries/Security/CWE-094/UnsafeJsEval.ql
|
||||
336
swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.swift
Normal file
336
swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.swift
Normal file
@@ -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<Any, Error>) -> 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<Any, Error>) -> 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<JSChar>!,
|
||||
_ numChars: Int
|
||||
) -> JSStringRef! {
|
||||
return chars.withMemoryRebound(to: CChar.self, capacity: numChars) {
|
||||
cchars in OpaquePointer(cchars)
|
||||
}
|
||||
}
|
||||
func JSStringCreateWithUTF8CString(_ string: UnsafePointer<CChar>!) -> 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?>!
|
||||
) -> 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<S>(_ 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 getRemoteData() -> String {
|
||||
let url = URL(string: "http://example.com/")
|
||||
do {
|
||||
return try String(contentsOf: url!)
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func testAsync(_ sink: @escaping (String) async throws -> ()) {
|
||||
Task {
|
||||
let localString = "console.log('localString')"
|
||||
let localStringFragment = "'localStringFragment'"
|
||||
let remoteString = getRemoteData()
|
||||
|
||||
try! await sink(localString) // GOOD: the HTML data is local
|
||||
try! await sink(try String(contentsOf: URL(string: "http://example.com/")!)) // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]: HTML contains remote input, may access local secrets
|
||||
try! await sink(remoteString) // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]
|
||||
|
||||
try! await sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local
|
||||
try! await sink("console.log(" + remoteString + ")") // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]
|
||||
|
||||
let localData = Data(localString.utf8)
|
||||
let remoteData = Data(remoteString.utf8)
|
||||
|
||||
try! await sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local
|
||||
try! await sink(String(decoding: remoteData, as: UTF8.self)) // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]: the data is remote
|
||||
|
||||
try! await sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion
|
||||
try! await sink("console.log(" + String(Int(remoteString) ?? 0) + ")") // GOOD: Primitive conversion
|
||||
|
||||
try! await sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
|
||||
try! await sink("console.log(" + (remoteString.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
|
||||
}
|
||||
}
|
||||
|
||||
func testSync(_ sink: @escaping (String) -> ()) {
|
||||
let localString = "console.log('localString')"
|
||||
let localStringFragment = "'localStringFragment'"
|
||||
let remoteString = getRemoteData()
|
||||
|
||||
sink(localString) // GOOD: the HTML data is local
|
||||
sink(try! String(contentsOf: URL(string: "http://example.com/")!)) // BAD: HTML contains remote input, may access local secrets
|
||||
sink(remoteString) // BAD
|
||||
|
||||
sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local
|
||||
sink("console.log(" + remoteString + ")") // BAD
|
||||
|
||||
let localData = Data(localString.utf8)
|
||||
let remoteData = Data(remoteString.utf8)
|
||||
|
||||
sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local
|
||||
sink(String(decoding: remoteData, as: UTF8.self)) // BAD: the data is remote
|
||||
|
||||
sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion
|
||||
sink("console.log(" + String(Int(remoteString) ?? 0) + ")") // GOOD: Primitive conversion
|
||||
|
||||
sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
|
||||
sink("console.log(" + (remoteString.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
|
||||
}
|
||||
|
||||
func testUIWebView() {
|
||||
let webview = UIWebView()
|
||||
|
||||
testAsync { string in
|
||||
_ = await webview.stringByEvaluatingJavaScript(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func testWebView() {
|
||||
let webview = WebView()
|
||||
|
||||
testAsync { string in
|
||||
_ = await webview.stringByEvaluatingJavaScript(from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func testWKWebView() {
|
||||
let webview = WKWebView()
|
||||
|
||||
testAsync { string in
|
||||
_ = try await webview.evaluateJavaScript(string)
|
||||
}
|
||||
testAsync { string in
|
||||
await webview.evaluateJavaScript(string) { _, _ in }
|
||||
}
|
||||
testAsync { string in
|
||||
await webview.evaluateJavaScript(string, in: nil, in: WKContentWorld.defaultClient) { _ in }
|
||||
}
|
||||
testAsync { string in
|
||||
_ = try await webview.evaluateJavaScript(string, contentWorld: .defaultClient)
|
||||
}
|
||||
testAsync { string in
|
||||
await webview.callAsyncJavaScript(string, in: nil, in: .defaultClient) { _ in () }
|
||||
}
|
||||
testAsync { string in
|
||||
_ = try await webview.callAsyncJavaScript(string, contentWorld: WKContentWorld.defaultClient)
|
||||
}
|
||||
}
|
||||
|
||||
func testWKUserContentController() {
|
||||
let ctrl = WKUserContentController()
|
||||
|
||||
testSync { string in
|
||||
ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentStart, forMainFrameOnly: false))
|
||||
}
|
||||
testSync { string in
|
||||
ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentEnd, forMainFrameOnly: true, in: .defaultClient))
|
||||
}
|
||||
}
|
||||
|
||||
func testJSContext() {
|
||||
let ctx = JSContext()
|
||||
|
||||
testSync { string in
|
||||
_ = ctx.evaluateScript(string)
|
||||
}
|
||||
testSync { string in
|
||||
_ = ctx.evaluateScript(string, withSourceURL: URL(string: "https://example.com"))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
/*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,
|
||||
/*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")!)
|
||||
|
||||
_ = try await webview.evaluateJavaScript("console.log(" + remoteData + ")") // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]
|
||||
|
||||
_ = try await webview.callAsyncJavaScript(
|
||||
"console.log(data)",
|
||||
arguments: ["data": remoteData], // GOOD
|
||||
contentWorld: .page
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
testUIWebView()
|
||||
testWebView()
|
||||
testWKWebView()
|
||||
testWKUserContentController()
|
||||
testJSContext()
|
||||
testJSEvaluateScript()
|
||||
testQHelpExamples()
|
||||
@@ -5,11 +5,7 @@ def _wrap_cc(rule, kwargs):
|
||||
_add_args(kwargs, "copts", [
|
||||
# Required by LLVM/Swift
|
||||
"-fno-rtti",
|
||||
] + select({
|
||||
# temporary, before we do universal merging and have an arm prebuilt package, we make arm build x86
|
||||
"@platforms//os:macos": ["-arch=x86_64"],
|
||||
"//conditions:default": [],
|
||||
}))
|
||||
])
|
||||
_add_args(kwargs, "features", [
|
||||
# temporary, before we do universal merging
|
||||
"-universal_binaries",
|
||||
|
||||
Reference in New Issue
Block a user