Merge pull request #11001 from d10c/swift/js-injection

This commit is contained in:
Nora Dimitrijević
2022-11-24 10:52:05 +01:00
committed by GitHub
11 changed files with 676 additions and 6 deletions

View File

@@ -1,3 +1,5 @@
{
"omnisharp.autoStart": false
"omnisharp.autoStart": false,
"cmake.sourceDirectory": "${workspaceFolder}/swift",
"cmake.buildDirectory": "${workspaceFolder}/bazel-cmake-build"
}

View File

@@ -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)

View File

@@ -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.
*

View 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>

View 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."

View File

@@ -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

View 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
)

View 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. |

View File

@@ -0,0 +1 @@
queries/Security/CWE-094/UnsafeJsEval.ql

View 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()

View File

@@ -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",