Swift: Move query logic into .qlls.

This commit is contained in:
Geoffrey White
2023-01-24 14:53:23 +00:00
parent 522c9d640d
commit cbfa7e7252
6 changed files with 344 additions and 319 deletions

View File

@@ -0,0 +1,142 @@
/**
* Provides a taint-tracking configuration for reasoning about database
* queries built from user-controlled sources (that is, SQL injection
* vulnerabilities).
*/
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
/**
* A `DataFlow::Node` that is a sink for a SQL string to be executed.
*/
abstract class SqlSink extends DataFlow::Node { }
/**
* A sink for the sqlite3 C API.
*/
class CApiSqlSink extends SqlSink {
CApiSqlSink() {
// `sqlite3_exec` and variants of `sqlite3_prepare`.
exists(CallExpr call |
call.getStaticTarget()
.(FreeFunctionDecl)
.hasName([
"sqlite3_exec(_:_:_:_:_:)", "sqlite3_prepare(_:_:_:_:_:)",
"sqlite3_prepare_v2(_:_:_:_:_:)", "sqlite3_prepare_v3(_:_:_:_:_:_:)",
"sqlite3_prepare16(_:_:_:_:_:)", "sqlite3_prepare16_v2(_:_:_:_:_:)",
"sqlite3_prepare16_v3(_:_:_:_:_:_:)"
]) and
call.getArgument(1).getExpr() = this.asExpr()
)
}
}
/**
* A sink for the SQLite.swift library.
*/
class SQLiteSwiftSqlSink extends SqlSink {
SQLiteSwiftSqlSink() {
// Variants of `Connection.execute`, `connection.prepare` and `connection.scalar`.
exists(CallExpr call |
call.getStaticTarget()
.(MethodDecl)
.hasQualifiedName("Connection",
["execute(_:)", "prepare(_:_:)", "run(_:_:)", "scalar(_:_:)"]) and
call.getArgument(0).getExpr() = this.asExpr()
)
or
// String argument to the `Statement` constructor.
exists(CallExpr call |
call.getStaticTarget().(MethodDecl).hasQualifiedName("Statement", "init(_:_:)") and
call.getArgument(1).getExpr() = this.asExpr()
)
}
}
/** A sink for the GRDB library. */
class GrdbSqlSink extends SqlSink {
GrdbSqlSink() {
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(0).getExpr() = this.asExpr()
|
method
.hasQualifiedName("Database",
[
"allStatements(sql:arguments:)", "cachedStatement(sql:)",
"internalCachedStatement(sql:)", "execute(sql:arguments:)", "makeStatement(sql:)",
"makeStatement(sql:prepFlags:)"
])
or
method
.hasQualifiedName("SQLRequest",
[
"init(stringLiteral:)", "init(unicodeScalarLiteral:)",
"init(extendedGraphemeClusterLiteral:)", "init(stringInterpolation:)",
"init(sql:arguments:adapter:cached:)"
])
or
method
.hasQualifiedName("SQL",
[
"init(stringLiteral:)", "init(unicodeScalarLiteral:)",
"init(extendedGraphemeClusterLiteral:)", "init(stringInterpolation:)",
"init(sql:arguments:)", "append(sql:arguments:)"
])
or
method
.hasQualifiedName("TableDefinition", ["column(sql:)", "check(sql:)", "constraint(sql:)"])
or
method.hasQualifiedName("TableAlteration", "addColumn(sql:)")
or
method
.hasQualifiedName("ColumnDefinition",
["check(sql:)", "defaults(sql:)", "generatedAs(sql:_:)"])
or
method
.hasQualifiedName("TableRecord",
[
"select(sql:arguments:)", "select(sql:arguments:as:)", "filter(sql:arguments:)",
"order(sql:arguments:)"
])
or
method.hasQualifiedName("StatementCache", "statement(_:)")
)
or
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(1).getExpr() = this.asExpr()
|
method
.hasQualifiedName(["Row", "DatabaseValueConvertible"],
[
"fetchCursor(_:sql:arguments:adapter:)", "fetchAll(_:sql:arguments:adapter:)",
"fetchSet(_:sql:arguments:adapter:)", "fetchOne(_:sql:arguments:adapter:)"
])
or
method.hasQualifiedName("SQLStatementCursor", "init(database:sql:arguments:prepFlags:)")
)
or
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(3).getExpr() = this.asExpr()
|
method
.hasQualifiedName("CommonTableExpression", "init(recursive:named:columns:sql:arguments:)")
)
}
}
/**
* A taint configuration for tainted data that reaches a SQL sink.
*/
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() { this = "SqlInjectionConfig" }
override predicate isSource(DataFlow::Node node) { node instanceof FlowSource }
override predicate isSink(DataFlow::Node node) { node instanceof SqlSink }
}

View File

@@ -0,0 +1,129 @@
/**
* Provides a taint-tracking configuration for reasoning about javascript
* evaluation vulnerabilities.
*/
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
/**
* A source of untrusted, user-controlled data.
*/
class Source = FlowSource;
/**
* 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("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().matches(["Unsafe%Buffer%", "Unsafe%Pointer%"]) and
member.getName() = "baseAddress"
|
e.getBase() = self and
e.getMember() = member and
nodeFrom.asExpr() = self and
nodeTo.asExpr() = e
)
}
}

View File

@@ -0,0 +1,70 @@
/**
* Provides a taint-tracking configuration for reasoning about unsafe
* webview fetch vulnerabilities.
*/
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
/**
* A sink that is a candidate result for this query, such as certain arguments
* to `UIWebView.loadHTMLString`.
*/
class Sink extends DataFlow::Node {
Expr baseUrl;
Sink() {
exists(
MethodDecl funcDecl, CallExpr call, string className, string funcName, int arg, int baseArg
|
// arguments to method calls...
(
// `loadHTMLString`
className = ["UIWebView", "WKWebView"] and
funcName = "loadHTMLString(_:baseURL:)" and
arg = 0 and
baseArg = 1
or
// `UIWebView.load`
className = "UIWebView" and
funcName = "load(_:mimeType:textEncodingName:baseURL:)" and
arg = 0 and
baseArg = 3
or
// `WKWebView.load`
className = "WKWebView" and
funcName = "load(_:mimeType:characterEncodingName:baseURL:)" and
arg = 0 and
baseArg = 3
) and
call.getStaticTarget() = funcDecl and
// match up `funcName`, `paramName`, `arg`, `node`.
funcDecl.hasQualifiedName(className, funcName) and
call.getArgument(arg).getExpr() = this.asExpr() and
// match up `baseURLArg`
call.getArgument(baseArg).getExpr() = baseUrl
)
}
/**
* Gets the `baseURL` argument associated with this sink.
*/
Expr getBaseUrl() { result = baseUrl }
}
/**
* A taint configuration from taint sources to sinks (and `baseURL` arguments)
* for this query.
*/
class UnsafeWebViewFetchConfig extends TaintTracking::Configuration {
UnsafeWebViewFetchConfig() { this = "UnsafeWebViewFetchConfig" }
override predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node node) {
node instanceof Sink or
node.asExpr() = any(Sink s).getBaseUrl()
}
}

View File

@@ -14,71 +14,9 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
import codeql.swift.security.UnsafeWebViewFetchQuery
import DataFlow::PathGraph
/**
* A sink that is a candidate result for this query, such as certain arguments
* to `UIWebView.loadHTMLString`.
*/
class Sink extends DataFlow::Node {
Expr baseUrl;
Sink() {
exists(
MethodDecl funcDecl, CallExpr call, string className, string funcName, int arg, int baseArg
|
// arguments to method calls...
(
// `loadHTMLString`
className = ["UIWebView", "WKWebView"] and
funcName = "loadHTMLString(_:baseURL:)" and
arg = 0 and
baseArg = 1
or
// `UIWebView.load`
className = "UIWebView" and
funcName = "load(_:mimeType:textEncodingName:baseURL:)" and
arg = 0 and
baseArg = 3
or
// `WKWebView.load`
className = "WKWebView" and
funcName = "load(_:mimeType:characterEncodingName:baseURL:)" and
arg = 0 and
baseArg = 3
) and
call.getStaticTarget() = funcDecl and
// match up `funcName`, `paramName`, `arg`, `node`.
funcDecl.hasQualifiedName(className, funcName) and
call.getArgument(arg).getExpr() = this.asExpr() and
// match up `baseURLArg`
call.getArgument(baseArg).getExpr() = baseUrl
)
}
/**
* Gets the `baseURL` argument associated with this sink.
*/
Expr getBaseUrl() { result = baseUrl }
}
/**
* A taint configuration from taint sources to sinks (and `baseURL` arguments)
* for this query.
*/
class UnsafeWebViewFetchConfig extends TaintTracking::Configuration {
UnsafeWebViewFetchConfig() { this = "UnsafeWebViewFetchConfig" }
override predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node node) {
node instanceof Sink or
node.asExpr() = any(Sink s).getBaseUrl()
}
}
from
UnsafeWebViewFetchConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode,
Sink sink, string message

View File

@@ -12,142 +12,9 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
import codeql.swift.security.SqlInjectionQuery
import DataFlow::PathGraph
/**
* A `DataFlow::Node` that is a sink for a SQL string to be executed.
*/
abstract class SqlSink extends DataFlow::Node { }
/**
* A sink for the sqlite3 C API.
*/
class CApiSqlSink extends SqlSink {
CApiSqlSink() {
// `sqlite3_exec` and variants of `sqlite3_prepare`.
exists(CallExpr call |
call.getStaticTarget()
.(FreeFunctionDecl)
.hasName([
"sqlite3_exec(_:_:_:_:_:)", "sqlite3_prepare(_:_:_:_:_:)",
"sqlite3_prepare_v2(_:_:_:_:_:)", "sqlite3_prepare_v3(_:_:_:_:_:_:)",
"sqlite3_prepare16(_:_:_:_:_:)", "sqlite3_prepare16_v2(_:_:_:_:_:)",
"sqlite3_prepare16_v3(_:_:_:_:_:_:)"
]) and
call.getArgument(1).getExpr() = this.asExpr()
)
}
}
/**
* A sink for the SQLite.swift library.
*/
class SQLiteSwiftSqlSink extends SqlSink {
SQLiteSwiftSqlSink() {
// Variants of `Connection.execute`, `connection.prepare` and `connection.scalar`.
exists(CallExpr call |
call.getStaticTarget()
.(MethodDecl)
.hasQualifiedName("Connection",
["execute(_:)", "prepare(_:_:)", "run(_:_:)", "scalar(_:_:)"]) and
call.getArgument(0).getExpr() = this.asExpr()
)
or
// String argument to the `Statement` constructor.
exists(CallExpr call |
call.getStaticTarget().(MethodDecl).hasQualifiedName("Statement", "init(_:_:)") and
call.getArgument(1).getExpr() = this.asExpr()
)
}
}
/** A sink for the GRDB library. */
class GrdbSqlSink extends SqlSink {
GrdbSqlSink() {
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(0).getExpr() = this.asExpr()
|
method
.hasQualifiedName("Database",
[
"allStatements(sql:arguments:)", "cachedStatement(sql:)",
"internalCachedStatement(sql:)", "execute(sql:arguments:)", "makeStatement(sql:)",
"makeStatement(sql:prepFlags:)"
])
or
method
.hasQualifiedName("SQLRequest",
[
"init(stringLiteral:)", "init(unicodeScalarLiteral:)",
"init(extendedGraphemeClusterLiteral:)", "init(stringInterpolation:)",
"init(sql:arguments:adapter:cached:)"
])
or
method
.hasQualifiedName("SQL",
[
"init(stringLiteral:)", "init(unicodeScalarLiteral:)",
"init(extendedGraphemeClusterLiteral:)", "init(stringInterpolation:)",
"init(sql:arguments:)", "append(sql:arguments:)"
])
or
method
.hasQualifiedName("TableDefinition", ["column(sql:)", "check(sql:)", "constraint(sql:)"])
or
method.hasQualifiedName("TableAlteration", "addColumn(sql:)")
or
method
.hasQualifiedName("ColumnDefinition",
["check(sql:)", "defaults(sql:)", "generatedAs(sql:_:)"])
or
method
.hasQualifiedName("TableRecord",
[
"select(sql:arguments:)", "select(sql:arguments:as:)", "filter(sql:arguments:)",
"order(sql:arguments:)"
])
or
method.hasQualifiedName("StatementCache", "statement(_:)")
)
or
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(1).getExpr() = this.asExpr()
|
method
.hasQualifiedName(["Row", "DatabaseValueConvertible"],
[
"fetchCursor(_:sql:arguments:adapter:)", "fetchAll(_:sql:arguments:adapter:)",
"fetchSet(_:sql:arguments:adapter:)", "fetchOne(_:sql:arguments:adapter:)"
])
or
method.hasQualifiedName("SQLStatementCursor", "init(database:sql:arguments:prepFlags:)")
)
or
exists(CallExpr call, MethodDecl method |
call.getStaticTarget() = method and
call.getArgument(3).getExpr() = this.asExpr()
|
method
.hasQualifiedName("CommonTableExpression", "init(recursive:named:columns:sql:arguments:)")
)
}
}
/**
* A taint configuration for tainted data that reaches a SQL sink.
*/
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() { this = "SqlInjectionConfig" }
override predicate isSource(DataFlow::Node node) { node instanceof FlowSource }
override predicate isSink(DataFlow::Node node) { node instanceof SqlSink }
}
from SqlInjectionConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode
where config.hasFlowPath(sourceNode, sinkNode)
select sinkNode.getNode(), sourceNode, sinkNode, "This query depends on a $@.",

View File

@@ -14,130 +14,9 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSources
import codeql.swift.security.UnsafeJsEvalQuery
import DataFlow::PathGraph
/**
* A source of untrusted, user-controlled data.
*/
class Source = FlowSource;
/**
* 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("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().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