Skip to content

Commit

Permalink
Merge pull request github#11670 from atorralba/atorralba/swift/predic…
Browse files Browse the repository at this point in the history
…ate-injection

Swift: Add predicate injection query
  • Loading branch information
MathiasVP authored Jan 9, 2023
2 parents 5b11708 + 30aa9b2 commit 9be9636
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 0 deletions.
1 change: 1 addition & 0 deletions swift/ql/lib/codeql/swift/dataflow/ExternalFlow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private module Frameworks {
private import codeql.swift.frameworks.StandardLibrary.WebView
private import codeql.swift.frameworks.Alamofire.Alamofire
private import codeql.swift.security.PathInjection
private import codeql.swift.security.PredicateInjection
}

/**
Expand Down
41 changes: 41 additions & 0 deletions swift/ql/lib/codeql/swift/security/PredicateInjection.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** Provides classes and predicates to reason about predicate injection vulnerabilities. */

import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.ExternalFlow

/** A data flow sink for predicate injection vulnerabilities. */
abstract class PredicateInjectionSink extends DataFlow::Node { }

/** A sanitizer for predicate injection vulnerabilities. */
abstract class PredicateInjectionSanitizer extends DataFlow::Node { }

/**
* A unit class for adding additional taint steps.
*
* Extend this class to add additional taint steps that should apply to paths related to
* predicate injection vulnerabilities.
*/
class PredicateInjectionAdditionalTaintStep extends Unit {
/**
* Holds if the step from `node1` to `node2` should be considered a taint
* step for paths related to predicate injection vulnerabilities.
*/
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
}

private class DefaultPredicateInjectionSink extends PredicateInjectionSink {
DefaultPredicateInjectionSink() { sinkNode(this, "predicate-injection") }
}

private class PredicateInjectionSinkCsv extends SinkModelCsv {
override predicate row(string row) {
row =
[
";NSPredicate;true;init(format:argumentArray:);;;Argument[0];predicate-injection",
";NSPredicate;true;init(format:arguments:);;;Argument[0];predicate-injection",
";NSPredicate;true;init(format:_:);;;Argument[0];predicate-injection",
";NSPredicate;true;init(fromMetadataQueryString:);;;Argument[0];predicate-injection"
]
}
}
29 changes: 29 additions & 0 deletions swift/ql/lib/codeql/swift/security/PredicateInjectionQuery.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Provides a taint-tracking configuration for reasoning about predicate injection
* vulnerabilities.
*/

import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.FlowSources
private import codeql.swift.dataflow.TaintTracking
private import codeql.swift.security.PredicateInjection

/**
* A taint-tracking configuration for predicate injection vulnerabilities.
*/
class PredicateInjectionConf extends TaintTracking::Configuration {
PredicateInjectionConf() { this = "PredicateInjectionConf" }

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof PredicateInjectionSink }

override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer instanceof PredicateInjectionSanitizer
}

override predicate isAdditionalTaintStep(DataFlow::Node n1, DataFlow::Node n2) {
any(PredicateInjectionAdditionalTaintStep s).step(n1, n2)
}
}
28 changes: 28 additions & 0 deletions swift/ql/src/queries/Security/CWE-943/PredicateInjection.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Predicates represent logical conditions that can be used to check whether an object matches them.
If a predicate is built from user-provided data without sufficient sanitization, an attacker may
be able to change the overall meaning of the predicate.
</p>
</overview>
<recommendation>
<p>
When building a predicate from untrusted data, you should either pass it to the appropriate <code>arguments</code> parameter during initialization, or as an array of substitution variables before evaluation. You should not append or concatenate it to the body of the predicate.
</p>
</recommendation>
<example>
<p>
In the following insecure example, <code>NSPredicate</code> is built directly from data obtained from an HTTP request. This is untrusted, and can be arbitrarily set by an attacker to alter the meaning of the predicate:
</p>
<sample src="PredicateInjectionBad.swift" />
<p>
A better way to do this is to use the <code>arguments</code> parameter of <code>NSPredicate</code>'s constructor. This prevents attackers from altering the meaning of the predicate, even if they control the externally obtained data, as seen in the following secure example:
</p>
<sample src="PredicateInjectionGood.swift" />
</example>
<references>
<li>Apple Developer Documentation: <a href="https://developer.apple.com/documentation/foundation/nspredicate">NSPredicate</a> </li>
</references>
</qhelp>
22 changes: 22 additions & 0 deletions swift/ql/src/queries/Security/CWE-943/PredicateInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @name Predicate built from user-controlled sources
* @description Building an NSPredicate from user-controlled sources may lead to attackers
* changing the predicate's intended logic.
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id swift/predicate-injection
* @tags security
* external/cwe/cwe-943
*/

import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.security.PredicateInjectionQuery
import DataFlow::PathGraph

from DataFlow::PathNode source, DataFlow::PathNode sink
where any(PredicateInjectionConf c).hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This predicate depends on a $@.", source.getNode(),
"user-provided value"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let remoteString = try String(contentsOf: URL(string: "https://example.com/")!)

let filenames: [String] = ["img1.png", "img2.png", "img3.png", "img.txt", "img.csv"]

let predicate = NSPredicate(format: "SELF LIKE \(remoteString)")
let filtered = filenames.filter(){ filename in
predicate.evaluate(with: filename)
}
print(filtered)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let remoteString = try String(contentsOf: URL(string: "https://example.com/")!)

let filenames: [String] = ["img1.png", "img2.png", "img3.png", "img.txt", "img.csv"]

let predicate = NSPredicate(format: "SELF LIKE %@", remoteString)
let filtered = filenames.filter(){ filename in
predicate.evaluate(with: filename)
}
print(filtered)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.security.PredicateInjectionQuery
import TestUtilities.InlineExpectationsTest

class PredicateInjectionTest extends InlineExpectationsTest {
PredicateInjectionTest() { this = "PredicateInjectionTest" }

override string getARelevantTag() { result = "hasPredicateInjection" }

override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(
PredicateInjectionConf config, DataFlow::Node source, DataFlow::Node sink, Expr sinkExpr
|
config.hasFlow(source, sink) and
sinkExpr = sink.asExpr() and
location = sinkExpr.getLocation() and
element = sinkExpr.toString() and
tag = "hasPredicateInjection" and
value = source.asExpr().getLocation().getStartLine().toString()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// --- stubs ---
struct URL {
init?(string: String) {}
}

extension String {
init(contentsOf: URL) {
let data = ""
self.init(data)
}
}

class NSPredicate {
init(format: String, argumentArray: [Any]?) {}
init(format: String, arguments: CVaListPointer) {}
init(format: String, _: CVarArg...) {}
init?(fromMetadataQueryString: String) {}
}

// --- tests ---

func test() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let safeString = "safe"

NSPredicate(format: remoteString, argumentArray: []) // $ hasPredicateInjection=23
NSPredicate(format: safeString, argumentArray: []) // Safe
NSPredicate(format: safeString, argumentArray: [remoteString]) // Safe
NSPredicate(format: remoteString, arguments: CVaListPointer(_fromUnsafeMutablePointer: UnsafeMutablePointer(bitPattern: 0)!)) // $ hasPredicateInjection=23
NSPredicate(format: safeString, arguments: CVaListPointer(_fromUnsafeMutablePointer: UnsafeMutablePointer(bitPattern: 0)!)) // Safe
NSPredicate(format: remoteString) // $ hasPredicateInjection=23
NSPredicate(format: safeString) // Safe
NSPredicate(format: remoteString, "" as! CVarArg) // $ hasPredicateInjection=23
NSPredicate(format: safeString, "" as! CVarArg) // Safe
NSPredicate(format: safeString, remoteString as! CVarArg) // Safe
NSPredicate(fromMetadataQueryString: remoteString) // $ hasPredicateInjection=23
NSPredicate(fromMetadataQueryString: safeString) // Safe
}

0 comments on commit 9be9636

Please sign in to comment.