-
Notifications
You must be signed in to change notification settings - Fork 2k
Swift: Unsafe JS Eval Query #11001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Swift: Unsafe JS Eval Query #11001
Changes from all commits
5c905c4
7b599f5
28b7f08
3d24e0a
fdd7d76
7c515bb
66291d3
d37ed02
7585541
5940f17
4b7a89e
16ba5b1
b42482c
8db8f14
cb7d9d5
fccb581
52e5d54
09b669a
8b33277
8f5af3f
61de07e
ef837f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| } |
| 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> |
| 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. | ||
|
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() | ||
| } | ||
| } | ||
|
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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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." | ||
| 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 |
| 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 | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.