Add Cleartext Loggin query for Swift.

With some caveats: see TODO comments and failing tests.
This commit is contained in:
Tony Torralba
2022-11-30 13:04:10 +01:00
parent 664fdc3b2a
commit aad56097ac
10 changed files with 328 additions and 0 deletions

View File

@@ -89,6 +89,7 @@ private module Frameworks {
private import codeql.swift.frameworks.StandardLibrary.UrlSession
private import codeql.swift.frameworks.StandardLibrary.WebView
private import codeql.swift.frameworks.Alamofire.Alamofire
private import codeql.swift.security.CleartextLogging
private import codeql.swift.security.PathInjection
private import codeql.swift.security.PredicateInjection
}

View File

@@ -0,0 +1,67 @@
/** Provides classes and predicates to reason about cleartext logging of sensitive data vulnerabilities. */
import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.ExternalFlow
private import codeql.swift.security.SensitiveExprs
/** A data flow sink for cleartext logging of sensitive data vulnerabilities. */
abstract class CleartextLoggingSink extends DataFlow::Node { }
/** A sanitizer for cleartext logging of sensitive data vulnerabilities. */
abstract class CleartextLoggingSanitizer 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
* cleartext logging of sensitive data vulnerabilities.
*/
class CleartextLoggingAdditionalTaintStep extends Unit {
/**
* Holds if the step from `n1` to `n2` should be considered a taint
* step for flows related to cleartext logging of sensitive data vulnerabilities.
*/
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
}
private class DefaultCleartextLoggingSink extends CleartextLoggingSink {
DefaultCleartextLoggingSink() { sinkNode(this, "logging") }
}
// TODO: Remove this. It shouldn't be necessary.
private class EncryptionCleartextLoggingSanitizer extends CleartextLoggingSanitizer {
EncryptionCleartextLoggingSanitizer() { this.asExpr() instanceof EncryptedExpr }
}
/*
* TODO: Add a sanitizer for the OsLogMessage interpolation with .private/.sensitive privacy options,
* or restrict the sinks to require .public interpolation depending on what the default behavior is.
*/
private class LoggingSinks extends SinkModelCsv {
override predicate row(string row) {
row =
[
";;false;print(_:separator:terminator:);;;Argument[0].ArrayElement;logging",
";;false;print(_:separator:terminator:);;;Argument[1..2];logging",
";;false;print(_:separator:terminator:toStream:);;;Argument[0].ArrayElement;logging",
";;false;print(_:separator:terminator:toStream:);;;Argument[1..2];logging",
";;false;NSLog(_:_:);;;Argument[0];logging",
";;false;NSLog(_:_:);;;Argument[1].ArrayElement;logging",
";;false;NSLogv(_:_:);;;Argument[0];logging",
";;false;NSLogv(_:_:);;;Argument[1].ArrayElement;logging",
";;false;vfprintf(_:_:_:);;;Agument[1..2];logging",
";Logger;true;log(_:);;;Argument[0];logging",
";Logger;true;log(level:_:);;;Argument[1];logging",
";Logger;true;trace(_:);;;Argument[1];logging",
";Logger;true;debug(_:);;;Argument[1];logging",
";Logger;true;info(_:);;;Argument[1];logging",
";Logger;true;notice(_:);;;Argument[1];logging",
";Logger;true;warning(_:);;;Argument[1];logging",
";Logger;true;error(_:);;;Argument[1];logging",
";Logger;true;critical(_:);;;Argument[1];logging",
";Logger;true;fault(_:);;;Argument[1];logging",
]
}
}

View File

@@ -0,0 +1,32 @@
/**
* Provides a taint-tracking configuration for reasoning about cleartext logging of
* sensitive data vulnerabilities.
*/
import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.TaintTracking
private import codeql.swift.security.CleartextLogging
private import codeql.swift.security.SensitiveExprs
/**
* A taint-tracking configuration for cleartext logging of sensitive data vulnerabilities.
*/
class CleartextLoggingConfiguration extends TaintTracking::Configuration {
CleartextLoggingConfiguration() { this = "CleartextLoggingConfiguration" }
override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof SensitiveExpr }
override predicate isSink(DataFlow::Node sink) { sink instanceof CleartextLoggingSink }
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer instanceof CleartextLoggingSanitizer
}
// Disregard paths that contain other paths. This helps with performance.
override predicate isSanitizerIn(DataFlow::Node node) { this.isSource(node) }
override predicate isAdditionalTaintStep(DataFlow::Node n1, DataFlow::Node n2) {
any(CleartextLoggingAdditionalTaintStep s).step(n1, n2)
}
}

View File

@@ -0,0 +1,50 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Sensitive information that is logged unencrypted is accessible to an attacker
who gains access to the logs.
</p>
</overview>
<recommendation>
<p>
Ensure that sensitive information is always encrypted or obfuscated before being
logged.
</p>
<p>
In general, decrypt sensitive information only at the point where it is
necessary for it to be used in cleartext.
</p>
<p>
Be aware that external processes often store the standard out and
standard error streams of the application, causing logged sensitive
information to be stored.
</p>
</recommendation>
<example>
<p>
The following example code logs user credentials (in this case, their password)
in plain text:
</p>
<sample src="CleartextLoggingBad.swift"/>
<p>
Instead, the credentials should be encrypted, obfuscated, or omitted entirely:
</p>
<sample src="CleartextLoggingGood.swift"/>
</example>
<references>
<li>M. Dowd, J. McDonald and J. Schuhm, <i>The Art of Software Security Assessment</i>, 1st Edition, Chapter 2 - 'Common Vulnerabilities of Encryption', p. 43. Addison Wesley, 2006.</li>
<li>M. Howard and D. LeBlanc, <i>Writing Secure Code</i>, 2nd Edition, Chapter 9 - 'Protecting Secret Data', p. 299. Microsoft, 2002.</li>
<li>OWASP: <a href="https://www.owasp.org/index.php/Password_Plaintext_Storage">Password Plaintext Storage</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,24 @@
/**
* @name Clear-text logging of sensitive information
* @description Logging sensitive information without encryption or hashing can
* expose it to an attacker.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id swift/clear-text-logging
* @tags security
* external/cwe/cwe-312
* external/cwe/cwe-359
* external/cwe/cwe-532
*/
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.security.CleartextLoggingQuery
import DataFlow::PathGraph
from DataFlow::PathNode src, DataFlow::PathNode sink
where any(CleartextLoggingConfiguration c).hasFlowPath(src, sink)
select sink.getNode(), src, sink, "This $@ is written to a log file.", src.getNode(),
"potentially sensitive information"

View File

@@ -0,0 +1,24 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.security.CleartextLoggingQuery
import TestUtilities.InlineExpectationsTest
class CleartextLogging extends InlineExpectationsTest {
CleartextLogging() { this = "CleartextLogging" }
override string getARelevantTag() { result = "hasCleartextLogging" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(
CleartextLoggingConfiguration 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 = "hasCleartextLogging" and
value = source.asExpr().getLocation().getStartLine().toString()
)
}
}

View File

@@ -0,0 +1,130 @@
// --- stubs ---
func NSLog(_ format: String, _ args: CVarArg...) {}
func NSLogv(_ format: String, _ args: CVaListPointer) {}
struct OSLogType : RawRepresentable {
static let `default` = OSLogType(rawValue: 0)
let rawValue: UInt8
init(rawValue: UInt8) { self.rawValue = rawValue}
}
struct OSLogStringAlignment {
static var none = OSLogStringAlignment()
}
struct OSLogPrivacy {
enum Mask { case none }
static var auto = OSLogPrivacy()
static var `private` = OSLogPrivacy()
static var sensitive = OSLogPrivacy()
static func auto(mask: OSLogPrivacy.Mask) -> OSLogPrivacy { return .auto }
static func `private`(mask: OSLogPrivacy.Mask) -> OSLogPrivacy { return .private }
static func `sensitive`(mask: OSLogPrivacy.Mask) -> OSLogPrivacy { return .sensitive }
}
struct OSLogInterpolation : StringInterpolationProtocol {
typealias StringLiteralType = String
init(literalCapacity: Int, interpolationCount: Int) {}
func appendLiteral(_: Self.StringLiteralType) {}
mutating func appendInterpolation(_ argumentString: @autoclosure @escaping () -> String, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ argumentString: @autoclosure @escaping () -> String, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto, attributes: String) {}
}
struct OSLogMessage : ExpressibleByStringInterpolation {
typealias StringInterpolation = OSLogInterpolation
typealias StringLiteralType = String
typealias ExtendedGraphemeClusterLiteralType = String
typealias UnicodeScalarLiteralType = String
init(stringInterpolation: OSLogInterpolation) {}
init(stringLiteral: String) {}
init(extendedGraphemeClusterLiteral: String) {}
init(unicodeScalarLiteral: String) {}
}
struct Logger {
func log(_ message: OSLogMessage) {}
func log(level: OSLogType, _ message: OSLogMessage) {}
func notice(_: OSLogMessage) {}
func debug(_: OSLogMessage) {}
func trace(_: OSLogMessage) {}
func info(_: OSLogMessage) {}
func error(_: OSLogMessage) {}
func warning(_: OSLogMessage) {}
func fault(_: OSLogMessage) {}
func critical(_: OSLogMessage) {}
}
// --- tests ---
func test1(password: String, passwordHash : String) {
print(password) // $ MISSING: hasCleartextLogging=63
print(password, separator: "") // $ MISSING: $ hasCleartextLogging=64
print("", separator: password) // $ hasCleartextLogging=65
print(password, separator: "", terminator: "") // $ MISSING: hasCleartextLogging=66
print("", separator: password, terminator: "") // $ hasCleartextLogging=67
print("", separator: "", terminator: password) // $ hasCleartextLogging=68
NSLog(password) // $ hasCleartextLogging=70
NSLog("%@", password as! CVarArg) // $ MISSING: hasCleartextLogging=71
NSLog("%@ %@", "" as! CVarArg, password as! CVarArg) // $ MISSING: hasCleartextLogging=72
NSLog("\(password)") // $ hasCleartextLogging=73
NSLogv("%@", getVaList([password as! CVarArg])) // $ MISSING: hasCleartextLogging=74
NSLogv("%@ %@", getVaList(["" as! CVarArg, password as! CVarArg])) // $ MISSING: hasCleartextLogging=75
let log = Logger()
log.log("\(password)") // $ hasCleartextLogging=78
log.log("\(password, privacy: .private)") // Safe
log.log(level: .default, "\(password)") // $ hasCleartextLogging=80
log.trace("\(password)") // $ hasCleartextLogging=81
log.debug("\(password)") // $ hasCleartextLogging=82
log.info("\(password)") // $ hasCleartextLogging=83
log.notice("\(password)") // $ hasCleartextLogging=84
log.warning("\(password)") // $ hasCleartextLogging=85
log.error("\(password)") // $ hasCleartextLogging=86
log.critical("\(password)") // $ hasCleartextLogging=87
log.fault("\(password)") // $ hasCleartextLogging=88
}
/*
class MyClass {
var harmless = "abc"
var password = "123"
}
func test3(x: String) {
// alternative evidence of sensitivity...
UserDefaults.standard.set(x, forKey: "myKey") // $ MISSING: hasCleartextLogging
doSomething(password: x);
UserDefaults.standard.set(x, forKey: "myKey") // $ hasCleartextLogging
let y = getPassword();
UserDefaults.standard.set(y, forKey: "myKey") // $ hasCleartextLogging
let z = MyClass()
UserDefaults.standard.set(z.harmless, forKey: "myKey") // Safe
UserDefaults.standard.set(z.password, forKey: "myKey") // $ hasCleartextLogging
}
func test4(passwd: String) {
// sanitizers...
var x = passwd;
var y = passwd;
var z = passwd;
UserDefaults.standard.set(x, forKey: "myKey") // $ hasCleartextLogging
UserDefaults.standard.set(y, forKey: "myKey") // $ hasCleartextLogging
UserDefaults.standard.set(z, forKey: "myKey") // $ hasCleartextLogging
x = encrypt(x);
hash(data: &y);
z = "";
UserDefaults.standard.set(x, forKey: "myKey") // Safe
UserDefaults.standard.set(y, forKey: "myKey") // Safe
UserDefaults.standard.set(z, forKey: "myKey") // Safe
}
*/