Skip to content

Commit 75a9809

Browse files
committed
Swift: Initial CleartextStoragePreferences impl.
Clearly based on CleartextStorageDatabase by @geoffw0.
1 parent 6e6880b commit 75a9809

File tree

3 files changed

+148
-0
lines changed

3 files changed

+148
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
5+
<p>Sensitive information that is stored unencrypted in the defaults database is accessible to an attacker who gains access to that database. For example, the information could be accessed by any process or user in a rooted device, or exposed through another vulnerability.</p>
6+
7+
</overview>
8+
<recommendation>
9+
10+
<p>Either store the data in an encrypted database, or ensure that each piece of sensitive information is encrypted before being stored. In general, decrypt sensitive information only at the point where it is necessary for it to be used in cleartext. Avoid storing sensitive information at all if you do not need to keep it.</p>
11+
12+
</recommendation>
13+
<example>
14+
15+
<p>The following example shows three cases of storing information using NSUserDefaults. In the 'BAD' case, the data that is stored is sensitive (a credit card number) and is not encrypted. In the 'GOOD' cases, the data is either not sensitive, or is protected with encryption.</p>
16+
17+
<sample src="CleartextStoragePreferences.swift" />
18+
19+
</example>
20+
<references>
21+
22+
<li>
23+
OWASP Top 10:2021:
24+
<a href="https://owasp.org/Top10/A02_2021-Cryptographic_Failures/">A02:2021 � Cryptographic Failures</a>.
25+
</li>
26+
27+
</references>
28+
</qhelp>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* @name Cleartext storage of sensitive information in application preferences
3+
* @description Storing sensitive information in a non-encrypted database can expose it to an attacker.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @security-severity 7.5
7+
* @precision medium
8+
* @id swift/cleartext-storage-preferences
9+
* @tags security
10+
* external/cwe/cwe-312
11+
*/
12+
13+
import swift
14+
import codeql.swift.security.SensitiveExprs
15+
import codeql.swift.dataflow.DataFlow
16+
import codeql.swift.dataflow.TaintTracking
17+
import DataFlow::PathGraph
18+
19+
/**
20+
* A `DataFlow::Node` of something that gets stored in a preferences store.
21+
*/
22+
abstract class Stored extends DataFlow::Node {
23+
abstract string getStoreName();
24+
}
25+
26+
/** The `DataFlow::Node` of an expression that gets written to the defaults database */
27+
class UserDefaultsStore extends Stored {
28+
UserDefaultsStore() {
29+
exists(ClassDecl c, AbstractFunctionDecl f, CallExpr call |
30+
c.getName() = ["UserDefaults"] and
31+
c.getAMember() = f and
32+
f.getName() = ["set(_:forKey:)"] and
33+
call.getStaticTarget() = f and
34+
call.getArgument(0).getExpr() = this.asExpr()
35+
)
36+
}
37+
38+
override string getStoreName() { result = "the user defaults database" }
39+
}
40+
41+
/** The `DataFlow::Node` of an expression that gets written to iCloud */
42+
class NSUbiquitousKeyValueStore extends Stored {
43+
NSUbiquitousKeyValueStore() {
44+
exists(ClassDecl c, AbstractFunctionDecl f, CallExpr call |
45+
c.getName() = ["NSUbiquitousKeyValueStore"] and
46+
c.getAMember() = f and
47+
f.getName() = ["set(_:forKey:)"] and
48+
call.getStaticTarget() = f and
49+
call.getArgument(0).getExpr() = this.asExpr()
50+
)
51+
}
52+
53+
override string getStoreName() { result = "iCloud" }
54+
}
55+
56+
/**
57+
* TODO: A more complicated case, this is a macOS-only way of writing to
58+
* NSUserDefaults by modifying the `NSUserDefaultsController.values: Any`
59+
* object via reflection (`perform(Selector)`) or the `NSKeyValueCoding`, `NSKeyValueBindingCreation` APIs.
60+
*/
61+
class NSUserDefaultsControllerStore extends Stored {
62+
NSUserDefaultsControllerStore() { none() }
63+
64+
override string getStoreName() { result = "the user defaults database" }
65+
}
66+
67+
/**
68+
* A taint configuration from sensitive information to expressions that are
69+
* stored as preferences.
70+
*/
71+
class CleartextStorageConfig extends TaintTracking::Configuration {
72+
CleartextStorageConfig() { this = "CleartextStorageConfig" }
73+
74+
override predicate isSource(DataFlow::Node node) { node.asExpr() instanceof SensitiveExpr }
75+
76+
override predicate isSink(DataFlow::Node node) { node instanceof Stored }
77+
78+
override predicate isSanitizerIn(DataFlow::Node node) {
79+
// make sources barriers so that we only report the closest instance
80+
isSource(node)
81+
}
82+
83+
override predicate isSanitizer(DataFlow::Node node) {
84+
// encryption barrier
85+
node.asExpr() instanceof EncryptedExpr
86+
}
87+
}
88+
89+
/**
90+
* Gets a prettier node to use in the results.
91+
*/
92+
DataFlow::Node cleanupNode(DataFlow::Node n) {
93+
result = n.(DataFlow::PostUpdateNode).getPreUpdateNode()
94+
or
95+
not n instanceof DataFlow::PostUpdateNode and
96+
result = n
97+
}
98+
99+
from CleartextStorageConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode
100+
where config.hasFlowPath(sourceNode, sinkNode)
101+
select cleanupNode(sinkNode.getNode()), sourceNode, sinkNode,
102+
"This operation stores '" + sinkNode.getNode().toString() + "' in " +
103+
sinkNode.getNode().(Stored).getStoreName() +
104+
". It may contain unencrypted sensitive data from $@", sourceNode,
105+
sourceNode.getNode().toString()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
func storeMyData(faveSong : String, creditCardNo : String) {
3+
// ...
4+
5+
// GOOD: not sensitive information
6+
UserDefaults.standard.set(faveSong, forKey: "myFaveSong")
7+
8+
// BAD: sensitive information saved in cleartext
9+
UserDefaults.standard.set(creditCardNo, forKey: "myCreditCardNo")
10+
11+
// GOOD: encrypted sensitive information saved
12+
UserDefaults.standard.set(encrypt(creditCardNo), forKey: "myCreditCardNo")
13+
14+
// ...
15+
}

0 commit comments

Comments
 (0)