Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5c905c4
Swift: Initial UnsafeJsEval query
d10c Oct 26, 2022
7b599f5
Swift: Add async varant of WKWebView evaluateJavaScript(_:)
d10c Oct 27, 2022
28b7f08
Swift: UnsafeJsEval test finally compiles
d10c Nov 2, 2022
3d24e0a
Swift: enable VSCode to build extractor via CMake
d10c Nov 3, 2022
fdd7d76
Swift: use FreeFunctionDecl/.has(Qualified)Name
d10c Nov 3, 2022
7c515bb
Swift: `_` as in `_ = ...` is a CFG leaf node.
d10c Nov 7, 2022
66291d3
Swift: sync tests pass with additional flow steps
d10c Nov 8, 2022
d37ed02
Swift: basic Data-related taint flow in query
d10c Nov 8, 2022
7585541
Merge branch 'main' into swift/js-injection
d10c Nov 8, 2022
5940f17
Swift: Docs + doctests
d10c Nov 9, 2022
4b7a89e
Merge branch 'main' into swift/js-injection
d10c Nov 11, 2022
16ba5b1
Swift: update doctests
d10c Nov 14, 2022
b42482c
Update swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
d10c Nov 15, 2022
8db8f14
Update swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
d10c Nov 15, 2022
cb7d9d5
Update swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
d10c Nov 15, 2022
fccb581
Update swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
d10c Nov 15, 2022
52e5d54
Update swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
d10c Nov 15, 2022
09b669a
Swift: Add direct call to remote source to a test
d10c Nov 15, 2022
8b33277
Swift: update `@security-severity`
d10c Nov 17, 2022
8f5af3f
Merge branch 'main' into swift/js-injection
d10c Nov 18, 2022
61de07e
Merge branch 'main' into swift/js-injection
d10c Nov 21, 2022
ef837f7
Swift: Test .expected changes resulting from merge.
geoffw0 Nov 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"omnisharp.autoStart": false
"omnisharp.autoStart": false,
"cmake.sourceDirectory": "${workspaceFolder}/swift",
"cmake.buildDirectory": "${workspaceFolder}/bazel-cmake-build"
}
4 changes: 4 additions & 0 deletions swift/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
geoffw0 marked this conversation as resolved.

/**
* The control-flow of an assignment operation.
*
Expand Down
32 changes: 32 additions & 0 deletions swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp
Original file line number Diff line number Diff line change
@@ -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 changes: 165 additions & 0 deletions swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
d10c marked this conversation as resolved.
*/
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()
}
}
Comment thread
d10c marked this conversation as resolved.

/**
* 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should aim to create follow-up tasks for most of these things rather than addressing them all now. Many are planned already. And I don't want this PR just growing and growing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Though it's a tradeoff between a growing PR and shipping more tech debt. An alternative is that I move these additional taint steps into the library, and create issues to flesh the library out to handle other cases that I didn't handle here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds fine as long as its straightforward.

I have a preference for smaller, faster PRs where possible as they're easier for everyone to review and manage.

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
Comment thread
geoffw0 marked this conversation as resolved.
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."
6 changes: 6 additions & 0 deletions swift/ql/src/queries/Security/CWE-094/UnsafeJsEvalBad.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions swift/ql/src/queries/Security/CWE-094/UnsafeJsEvalGood.swift
Original file line number Diff line number Diff line change
@@ -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
)
Loading