Skip to content

Swift: detect the use of constant salts #10993

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

Merged
merged 2 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions swift/ql/src/queries/Security/CWE-760/ConstantSalt.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Constant salts should not be used for password hashing. Data hashed using constant salts are vulnerable to dictionary attacks, enabling attackers to recover the original input.</p>
</overview>

<recommendation>
<p>Use randomly generated salts to securely hash input data.</p>
</recommendation>

<example>
<p>The following example shows a few cases of hashing input data. In the 'BAD' cases, the salt is constant, making the generated hashes vulnerable to dictionary attacks. In the 'GOOD' cases, the salt is randomly generated, which protects the hashed data against recovery.</p>
<sample src="ConstantSalt.swift" />
</example>

<references>
<li><a href="https://www.okta.com/blog/2019/03/what-are-salted-passwords-and-password-hashing/">What are Salted Passwords and Password Hashing?</a></li>
</references>
</qhelp>
63 changes: 63 additions & 0 deletions swift/ql/src/queries/Security/CWE-760/ConstantSalt.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @name Use of constant salts
* @description Using constant salts for password hashing is not secure because potential attackers can precompute the hash value via dictionary attacks.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id swift/constant-salt
* @tags security
* external/cwe/cwe-760
*/

import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
import codeql.swift.dataflow.FlowSteps
import DataFlow::PathGraph

/**
* A constant salt is created through either a byte array or string literals.
*/
class ConstantSaltSource extends Expr {
ConstantSaltSource() {
this = any(ArrayExpr arr | arr.getType().getName() = "Array<UInt8>") or
this instanceof StringLiteralExpr
}
}

/**
* A class for all ways to use a constant salt.
*/
class ConstantSaltSink extends Expr {
ConstantSaltSink() {
// `salt` arg in `init` is a sink
exists(ClassOrStructDecl c, AbstractFunctionDecl f, CallExpr call, int arg |
c.getFullName() = ["HKDF", "PBKDF1", "PBKDF2", "Scrypt"] and
c.getAMember() = f and
f.getName().matches("%init(%salt:%") and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a great start, my only concern is how often salts are specified in the wild using the functions modelled above. I did a quick MRVA query for parameters called "salt" (or similar) and the most common call that appears to be relevant is to a thing called CCKeyDerivationPBKDF(_:_:_:_:_:_:_:_:_:) from CommonCrypto. Do you think we should perhaps create a follow-up issue to add a model of that to this query as well?

Copy link
Contributor Author

@karimhamdanali karimhamdanali Oct 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add further support for CommonCrytpo, not just for this query, but perhaps other crypto queries too. This might be a good first step in supporting more crypto APIs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind creating an issue for that, listing the support you think we need to add?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Just added one with an initial set of tasks along with some useful resources.

call.getStaticTarget() = f and
f.getParam(pragma[only_bind_into](arg)).getName() = "salt" and
call.getArgument(pragma[only_bind_into](arg)).getExpr() = this
)
}
}

/**
* A taint configuration from the source of constants salts to expressions that use
* them to initialize password-based enecryption keys.
*/
class ConstantSaltConfig extends TaintTracking::Configuration {
ConstantSaltConfig() { this = "ConstantSaltConfig" }

override predicate isSource(DataFlow::Node node) { node.asExpr() instanceof ConstantSaltSource }

override predicate isSink(DataFlow::Node node) { node.asExpr() instanceof ConstantSaltSink }
}

// The query itself
from ConstantSaltConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode
where config.hasFlowPath(sourceNode, sinkNode)
select sinkNode.getNode(), sourceNode, sinkNode,
"The value '" + sourceNode.getNode().toString() +
"' is used as a constant salt, which is insecure for hashing passwords."
22 changes: 22 additions & 0 deletions swift/ql/src/queries/Security/CWE-760/ConstantSalt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

func encrypt(padding : Padding) {
// ...

// BAD: Using constant salts for hashing
let salt: Array<UInt8> = [0x2a, 0x3a, 0x80, 0x05]
let randomArray = (0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
_ = try HKDF(password: randomArray, salt: salt, info: randomArray, keyLength: 0, variant: Variant.sha2)
_ = try PKCS5.PBKDF1(password: randomArray, salt: salt, iterations: 120120, keyLength: 0)
_ = try PKCS5.PBKDF2(password: randomArray, salt: salt, iterations: 120120, keyLength: 0)
_ = try Scrypt(password: randomArray, salt: salt, dkLen: 64, N: 16384, r: 8, p: 1)

// GOOD: Using randomly generated salts for hashing
let salt = (0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
let randomArray = (0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
_ = try HKDF(password: randomArray, salt: salt, info: randomArray, keyLength: 0, variant: Variant.sha2)
_ = try PKCS5.PBKDF1(password: randomArray, salt: salt, iterations: 120120, keyLength: 0)
_ = try PKCS5.PBKDF2(password: randomArray, salt: salt, iterations: 120120, keyLength: 0)
_ = try Scrypt(password: randomArray, salt: salt, dkLen: 64, N: 16384, r: 8, p: 1)

// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
edges
| test.swift:43:35:43:130 | [...] : | test.swift:51:49:51:49 | constantSalt |
| test.swift:43:35:43:130 | [...] : | test.swift:56:59:56:59 | constantSalt |
| test.swift:43:35:43:130 | [...] : | test.swift:62:59:62:59 | constantSalt |
| test.swift:43:35:43:130 | [...] : | test.swift:67:53:67:53 | constantSalt |
nodes
| test.swift:43:35:43:130 | [...] : | semmle.label | [...] : |
| test.swift:51:49:51:49 | constantSalt | semmle.label | constantSalt |
| test.swift:56:59:56:59 | constantSalt | semmle.label | constantSalt |
| test.swift:62:59:62:59 | constantSalt | semmle.label | constantSalt |
| test.swift:67:53:67:53 | constantSalt | semmle.label | constantSalt |
subpaths
#select
| test.swift:51:49:51:49 | constantSalt | test.swift:43:35:43:130 | [...] : | test.swift:51:49:51:49 | constantSalt | The value '[...]' is used as a constant salt, which is insecure for hashing passwords. |
| test.swift:56:59:56:59 | constantSalt | test.swift:43:35:43:130 | [...] : | test.swift:56:59:56:59 | constantSalt | The value '[...]' is used as a constant salt, which is insecure for hashing passwords. |
| test.swift:62:59:62:59 | constantSalt | test.swift:43:35:43:130 | [...] : | test.swift:62:59:62:59 | constantSalt | The value '[...]' is used as a constant salt, which is insecure for hashing passwords. |
| test.swift:67:53:67:53 | constantSalt | test.swift:43:35:43:130 | [...] : | test.swift:67:53:67:53 | constantSalt | The value '[...]' is used as a constant salt, which is insecure for hashing passwords. |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
queries/Security/CWE-760/ConstantSalt.ql
70 changes: 70 additions & 0 deletions swift/ql/test/query-tests/Security/CWE-760/test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

// --- stubs ---

// These stubs roughly follows the same structure as classes from CryptoSwift
enum PKCS5 { }

enum Variant { case md5, sha1, sha2, sha3 }

extension PKCS5 {
struct PBKDF1 {
init(password: Array<UInt8>, salt: Array<UInt8>, variant: Variant = .sha1, iterations: Int = 4096, keyLength: Int? = nil) { }
}

struct PBKDF2 {
init(password: Array<UInt8>, salt: Array<UInt8>, iterations: Int = 4096, keyLength: Int? = nil, variant: Variant = .sha2) { }
}
}

struct HKDF {
init(password: Array<UInt8>, salt: Array<UInt8>? = nil, info: Array<UInt8>? = nil, keyLength: Int? = nil, variant: Variant = .sha2) { }
}

final class Scrypt {
init(password: Array<UInt8>, salt: Array<UInt8>, dkLen: Int, N: Int, r: Int, p: Int) { }
}

// Helper functions
func getConstantString() -> String {
"this string is constant"
}

func getConstantArray() -> Array<UInt8> {
[UInt8](getConstantString().utf8)
}

func getRandomArray() -> Array<UInt8> {
(0..<10).map({ _ in UInt8.random(in: 0...UInt8.max) })
}

// --- tests ---

func test() {
let constantSalt: Array<UInt8> = [0x2a, 0x3a, 0x80, 0x05, 0xaf, 0x46, 0x58, 0x2d, 0x66, 0x52, 0x10, 0xae, 0x86, 0xd3, 0x8e, 0x8f]
let constantStringSalt = getConstantArray()
let randomSalt = getRandomArray()
let randomArray = getRandomArray()
let variant = Variant.sha2
let iterations = 120120

// HKDF test cases
let hkdfb1 = HKDF(password: randomArray, salt: constantSalt, info: randomArray, keyLength: 0, variant: variant) // BAD
let hkdfb2 = HKDF(password: randomArray, salt: constantStringSalt, info: randomArray, keyLength: 0, variant: variant) // BAD [NOT DETECTED]
let hkdfg1 = HKDF(password: randomArray, salt: randomSalt, info: randomArray, keyLength: 0, variant: variant) // GOOD

// PBKDF1 test cases
let pbkdf1b1 = PKCS5.PBKDF1(password: randomArray, salt: constantSalt, iterations: iterations, keyLength: 0) // BAD
let pbkdf1b2 = PKCS5.PBKDF1(password: randomArray, salt: constantStringSalt, iterations: iterations, keyLength: 0) // BAD [NOT DETECTED]
let pbkdf1g1 = PKCS5.PBKDF1(password: randomArray, salt: randomSalt, iterations: iterations, keyLength: 0) // GOOD


// PBKDF2 test cases
let pbkdf2b1 = PKCS5.PBKDF2(password: randomArray, salt: constantSalt, iterations: iterations, keyLength: 0) // BAD
let pbkdf2b2 = PKCS5.PBKDF2(password: randomArray, salt: constantStringSalt, iterations: iterations, keyLength: 0) // BAD [NOT DETECTED]
let pbkdf2g1 = PKCS5.PBKDF2(password: randomArray, salt: randomSalt, iterations: iterations, keyLength: 0) // GOOD

// Scrypt test cases
let scryptb1 = Scrypt(password: randomArray, salt: constantSalt, dkLen: 64, N: 16384, r: 8, p: 1) // BAD
let scryptb2 = Scrypt(password: randomArray, salt: constantStringSalt, dkLen: 64, N: 16384, r: 8, p: 1) // BAD [NOT DETECTED]
let scryptg1 = Scrypt(password: randomArray, salt: randomSalt, dkLen: 64, N: 16384, r: 8, p: 1) // GOOD
}