Merge branch 'main' into redsun82/swift-open-redirection

This commit is contained in:
Paolo Tranquilli
2023-01-17 09:43:00 +01:00
committed by GitHub
38 changed files with 968 additions and 26 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,103 @@
/** 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") }
}
/**
* A sanitizer for `OSLogMessage`s configured with the appropriate privacy option.
* Numeric and boolean arguments aren't redacted unless the `private` or `sensitive` options are used.
* Arguments of other types are always redacted unless the `public` option is used.
*/
private class OsLogPrivacyCleartextLoggingSanitizer extends CleartextLoggingSanitizer {
OsLogPrivacyCleartextLoggingSanitizer() {
exists(CallExpr c, AutoClosureExpr e |
c.getStaticTarget().getName().matches("appendInterpolation(_:%privacy:%)") and
c.getArgument(0).getExpr() = e and
this.asExpr() = e
|
e.getExpr().getType() instanceof OsLogNonRedactedType and
c.getArgumentWithLabel("privacy").getExpr().(OsLogPrivacyRef).isSafe()
or
not e.getExpr().getType() instanceof OsLogNonRedactedType and
not c.getArgumentWithLabel("privacy").getExpr().(OsLogPrivacyRef).isPublic()
)
}
}
/** A type that isn't redacted by default in an `OSLogMessage`. */
private class OsLogNonRedactedType extends Type {
OsLogNonRedactedType() {
this.getName() = [["", "U"] + "Int" + ["", "8", "16", "32", "64"], "Double", "Float", "Bool"]
}
}
/** A reference to a field of `OsLogPrivacy`. */
private class OsLogPrivacyRef extends MemberRefExpr {
string optionName;
OsLogPrivacyRef() {
exists(FieldDecl f | this.getMember() = f |
f.getEnclosingDecl().(NominalTypeDecl).getName() = "OSLogPrivacy" and
optionName = f.getName()
)
}
/** Holds if this is a safe privacy option (private or sensitive). */
predicate isSafe() { optionName = ["private", "sensitive"] }
/** Holds if this is a public (that is, unsafe) privacy option. */
predicate isPublic() { optionName = "public" }
}
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,46 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Attackers could gain access to sensitive information that is logged unencrypted.
</p>
</overview>
<recommendation>
<p>
Always make sure to encrypt or obfuscate sensitive information before you log it.
</p>
<p>
Generally, you should 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 output and
standard error streams of the application. This will include logged sensitive information.
</p>
</recommendation>
<example>
<p>
The following example code logs user credentials (in this case, their password)
in plaintext:
</p>
<sample src="CleartextLoggingBad.swift"/>
<p>
Instead, you should encrypt or obfuscate the credentials, or omit them 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 Cleartext logging of sensitive information
* @description Logging sensitive information in plaintext 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,2 @@
let password = "P@ssw0rd"
NSLog("User password changed to \(password)")

View File

@@ -0,0 +1,2 @@
let password = "P@ssw0rd"
NSLog("User password changed")

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,158 @@
// --- stubs ---
func NSLog(_ format: String, _ args: CVarArg...) {}
func NSLogv(_ format: String, _ args: CVaListPointer) {}
func getVaList(_ args: [CVarArg]) -> CVaListPointer { return CVaListPointer(_fromUnsafeMutablePointer: UnsafeMutablePointer(bitPattern: 0)!) }
struct OSLogType : RawRepresentable {
static let `default` = OSLogType(rawValue: 0)
let rawValue: UInt8
init(rawValue: UInt8) { self.rawValue = rawValue}
}
struct OSLogStringAlignment {
static var none = OSLogStringAlignment()
}
enum OSLogIntegerFormatting { case decimal }
enum OSLogInt32ExtendedFormat { case none }
enum OSLogFloatFormatting { case fixed }
enum OSLogBoolFormat { case truth }
struct OSLogPrivacy {
enum Mask { case none }
static var auto = OSLogPrivacy()
static var `private` = OSLogPrivacy()
static var `public` = 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) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int8, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int16, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int32, format: OSLogInt32ExtendedFormat, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int32, format: OSLogInt32ExtendedFormat, privacy: OSLogPrivacy = .auto, attributes: String) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int32, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Int64, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> UInt, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> UInt8, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> UInt16, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> UInt32, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> UInt64, format: OSLogIntegerFormatting = .decimal, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Double, format: OSLogFloatFormatting = .fixed, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Double, format: OSLogFloatFormatting = .fixed, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto, attributes: String) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Float,format: OSLogFloatFormatting = .fixed, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto) {}
mutating func appendInterpolation(_ number: @autoclosure @escaping () -> Float, format: OSLogFloatFormatting = .fixed, align: OSLogStringAlignment = .none, privacy: OSLogPrivacy = .auto, attributes: String) {}
mutating func appendInterpolation(_ boolean: @autoclosure @escaping () -> Bool, format: OSLogBoolFormat = .truth, privacy: OSLogPrivacy = .auto) {}
}
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=87
print(password, separator: "") // $ MISSING: $ hasCleartextLogging=88
print("", separator: password) // $ hasCleartextLogging=89
print(password, separator: "", terminator: "") // $ MISSING: hasCleartextLogging=90
print("", separator: password, terminator: "") // $ hasCleartextLogging=91
print("", separator: "", terminator: password) // $ hasCleartextLogging=92
print(passwordHash) // Safe
NSLog(password) // $ hasCleartextLogging=95
NSLog("%@", password as! CVarArg) // $ MISSING: hasCleartextLogging=96
NSLog("%@ %@", "" as! CVarArg, password as! CVarArg) // $ MISSING: hasCleartextLogging=97
NSLog("\(password)") // $ hasCleartextLogging=98
NSLogv("%@", getVaList([password as! CVarArg])) // $ MISSING: hasCleartextLogging=99
NSLogv("%@ %@", getVaList(["" as! CVarArg, password as! CVarArg])) // $ MISSING: hasCleartextLogging=100
NSLog(passwordHash) // SAfe
NSLogv("%@", getVaList([passwordHash as! CVarArg])) // Safe
let bankAccount: Int = 0
let log = Logger()
// These MISSING test cases will be fixed when we properly generate the CFG around autoclosures.
log.log("\(password)") // Safe
log.log("\(password, privacy: .auto)") // Safe
log.log("\(password, privacy: .private)") // Safe
log.log("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=110
log.log("\(passwordHash, privacy: .public)") // Safe
log.log("\(password, privacy: .sensitive)") // Safe
log.log("\(bankAccount)") // $ MISSING: hasCleartextLogging=113
log.log("\(bankAccount, privacy: .auto)") // $ MISSING: hasCleartextLogging=114
log.log("\(bankAccount, privacy: .private)") // Safe
log.log("\(bankAccount, privacy: .public)") // $ MISSING: hasCleartextLogging=116
log.log("\(bankAccount, privacy: .sensitive)") // Safe
log.log(level: .default, "\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=118
log.trace("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=119
log.trace("\(passwordHash, privacy: .public)") // Safe
log.debug("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=121
log.debug("\(passwordHash, privacy: .public)") // Safe
log.info("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=123
log.info("\(passwordHash, privacy: .public)") // Safe
log.notice("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=125
log.notice("\(passwordHash, privacy: .public)") // Safe
log.warning("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=127
log.warning("\(passwordHash, privacy: .public)") // Safe
log.error("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=129
log.error("\(passwordHash, privacy: .public)") // Safe
log.critical("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=131
log.critical("\(passwordHash, privacy: .public)") // Safe
log.fault("\(password, privacy: .public)") // $ MISSING: hasCleartextLogging=133
log.fault("\(passwordHash, privacy: .public)") // Safe
}
class MyClass {
var harmless = "abc"
var password = "123"
}
func getPassword() -> String { return "" }
func doSomething(password: String) { }
func test3(x: String) {
// alternative evidence of sensitivity...
NSLog(x) // $ MISSING: hasCleartextLogging=148
doSomething(password: x);
NSLog(x) // $ hasCleartextLogging=149
let y = getPassword();
NSLog(y) // $ hasCleartextLogging=152
let z = MyClass()
NSLog(z.harmless) // Safe
NSLog(z.password) // $ hasCleartextLogging=157
}