Skip to content

Commit 28fb0ed

Browse files
authored
Merge pull request github#4920 from luchua-bc/java/hash-without-salt
Java: Query to detect hash without salt
2 parents bc9682c + 344c2d3 commit 28fb0ed

File tree

9 files changed

+453
-0
lines changed

9 files changed

+453
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public class HashWithoutSalt {
2+
// BAD - Hash without a salt.
3+
public void getSHA256Hash(String password) throws NoSuchAlgorithmException {
4+
MessageDigest md = MessageDigest.getInstance("SHA-256");
5+
byte[] messageDigest = md.digest(password.getBytes());
6+
}
7+
8+
// GOOD - Hash with a salt.
9+
public void getSHA256Hash(String password, byte[] salt) throws NoSuchAlgorithmException {
10+
MessageDigest md = MessageDigest.getInstance("SHA-256");
11+
md.update(salt);
12+
byte[] messageDigest = md.digest(password.getBytes());
13+
}
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>In cryptography, a salt is some random data used as an additional input to a one-way function that hashes a password or pass-phrase. It makes dictionary attacks more difficult.</p>
6+
7+
<p>Without a salt, it is much easier for attackers to pre-compute the hash value using dictionary attack techniques such as rainbow tables to crack passwords.</p>
8+
</overview>
9+
10+
<recommendation>
11+
<p>Use a long random salt of at least 32 bytes then use the combination of password and salt to hash a password or password phrase.</p>
12+
</recommendation>
13+
14+
<example>
15+
<p>The following example shows two ways of hashing. In the 'BAD' case, no salt is provided. In the 'GOOD' case, a salt is provided.</p>
16+
<sample src="HashWithoutSalt.java" />
17+
</example>
18+
19+
<references>
20+
<li>
21+
DZone:
22+
<a href="https://dzone.com/articles/a-look-at-java-cryptography">A Look at Java Cryptography</a>
23+
</li>
24+
<li>
25+
CWE:
26+
<a href="https://cwe.mitre.org/data/definitions/759.html">CWE-759: Use of a One-Way Hash without a Salt</a>
27+
</li>
28+
</references>
29+
</qhelp>
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @name Use of a hash function without a salt
3+
* @description Hashed passwords without a salt are vulnerable to dictionary attacks.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @precision low
7+
* @id java/hash-without-salt
8+
* @tags security
9+
* external/cwe-759
10+
*/
11+
12+
import java
13+
import semmle.code.java.dataflow.TaintTracking
14+
import DataFlow::PathGraph
15+
16+
/**
17+
* Gets a regular expression for matching common names of variables
18+
* that indicate the value being held is a password.
19+
*/
20+
string getPasswordRegex() { result = "(?i).*pass(wd|word|code|phrase).*" }
21+
22+
/** Finds variables that hold password information judging by their names. */
23+
class PasswordVarExpr extends VarAccess {
24+
PasswordVarExpr() {
25+
exists(string name | name = this.getVariable().getName().toLowerCase() |
26+
name.regexpMatch(getPasswordRegex()) and not name.matches("%hash%") // Exclude variable names such as `passwordHash` since their values were already hashed
27+
)
28+
}
29+
}
30+
31+
/** Holds if `Expr` e is a direct or indirect operand of `ae`. */
32+
predicate hasAddExprAncestor(AddExpr ae, Expr e) { ae.getAnOperand+() = e }
33+
34+
/** The Java class `java.security.MessageDigest`. */
35+
class MessageDigest extends RefType {
36+
MessageDigest() { this.hasQualifiedName("java.security", "MessageDigest") }
37+
}
38+
39+
/** The method call `MessageDigest.getInstance(...)` */
40+
class MDConstructor extends StaticMethodAccess {
41+
MDConstructor() {
42+
exists(Method m | m = this.getMethod() |
43+
m.getDeclaringType() instanceof MessageDigest and
44+
m.hasName("getInstance")
45+
)
46+
}
47+
}
48+
49+
/** The method `digest()` declared in `java.security.MessageDigest`. */
50+
class MDDigestMethod extends Method {
51+
MDDigestMethod() {
52+
this.getDeclaringType() instanceof MessageDigest and
53+
this.hasName("digest")
54+
}
55+
}
56+
57+
/** The method `update()` declared in `java.security.MessageDigest`. */
58+
class MDUpdateMethod extends Method {
59+
MDUpdateMethod() {
60+
this.getDeclaringType() instanceof MessageDigest and
61+
this.hasName("update")
62+
}
63+
}
64+
65+
/** The hashing method that could taint the input. */
66+
class MDHashMethodAccess extends MethodAccess {
67+
MDHashMethodAccess() {
68+
(
69+
this.getMethod() instanceof MDDigestMethod or
70+
this.getMethod() instanceof MDUpdateMethod
71+
) and
72+
this.getNumArgument() != 0
73+
}
74+
}
75+
76+
/**
77+
* Holds if `MethodAccess` ma is a method access of `MDHashMethodAccess` or
78+
* invokes a method access of `MDHashMethodAccess` directly or indirectly.
79+
*/
80+
predicate isHashAccess(MethodAccess ma) {
81+
ma instanceof MDHashMethodAccess
82+
or
83+
exists(MethodAccess mca |
84+
ma.getMethod().calls(mca.getMethod()) and
85+
isHashAccess(mca) and
86+
DataFlow::localExprFlow(ma.getMethod().getAParameter().getAnAccess(), mca.getAnArgument())
87+
)
88+
}
89+
90+
/**
91+
* Holds if there is a second method access that satisfies `isHashAccess` whose qualifier or argument
92+
* is the same as the method call `ma` that satisfies `isHashAccess`.
93+
*/
94+
predicate hasAnotherHashCall(MethodAccess ma) {
95+
isHashAccess(ma) and
96+
exists(MethodAccess ma2, VarAccess va |
97+
ma2 != ma and
98+
isHashAccess(ma2) and
99+
not va.getVariable().getType() instanceof PrimitiveType and
100+
(
101+
ma.getQualifier() = va and
102+
ma2.getQualifier() = va.getVariable().getAnAccess()
103+
or
104+
ma.getQualifier() = va and
105+
ma2.getAnArgument() = va.getVariable().getAnAccess()
106+
or
107+
ma.getAnArgument() = va and
108+
ma2.getQualifier() = va.getVariable().getAnAccess()
109+
or
110+
ma.getAnArgument() = va and
111+
ma2.getAnArgument() = va.getVariable().getAnAccess()
112+
)
113+
)
114+
}
115+
116+
/**
117+
* Holds if `MethodAccess` ma is part of a call graph that satisfies `isHashAccess`
118+
* but is not at the top of the call hierarchy.
119+
*/
120+
predicate hasHashAncestor(MethodAccess ma) {
121+
exists(MethodAccess mpa |
122+
mpa.getMethod().calls(ma.getMethod()) and
123+
isHashAccess(mpa) and
124+
DataFlow::localExprFlow(mpa.getMethod().getAParameter().getAnAccess(), ma.getAnArgument())
125+
)
126+
}
127+
128+
/** Holds if `MethodAccess` ma is a hashing call without a sibling node making another hashing call. */
129+
predicate isSingleHashMethodCall(MethodAccess ma) {
130+
isHashAccess(ma) and not hasAnotherHashCall(ma)
131+
}
132+
133+
/** Holds if `MethodAccess` ma is a single hashing call that is not invoked by a wrapper method. */
134+
predicate isSink(MethodAccess ma) { isSingleHashMethodCall(ma) and not hasHashAncestor(ma) }
135+
136+
/** Sink of hashing calls. */
137+
class HashWithoutSaltSink extends DataFlow::ExprNode {
138+
HashWithoutSaltSink() {
139+
exists(MethodAccess ma |
140+
this.asExpr() = ma.getAnArgument() and
141+
isSink(ma)
142+
)
143+
}
144+
}
145+
146+
/**
147+
* Taint configuration tracking flow from an expression whose name suggests it holds password data
148+
* to a method call that generates a hash without a salt.
149+
*/
150+
class HashWithoutSaltConfiguration extends TaintTracking::Configuration {
151+
HashWithoutSaltConfiguration() { this = "HashWithoutSaltConfiguration" }
152+
153+
override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof PasswordVarExpr }
154+
155+
override predicate isSink(DataFlow::Node sink) { sink instanceof HashWithoutSaltSink }
156+
157+
/**
158+
* Holds if a password is concatenated with a salt then hashed together through the call `System.arraycopy(password.getBytes(), ...)`, for example,
159+
* `System.arraycopy(password.getBytes(), 0, allBytes, 0, password.getBytes().length);`
160+
* `System.arraycopy(salt, 0, allBytes, password.getBytes().length, salt.length);`
161+
* `byte[] messageDigest = md.digest(allBytes);`
162+
* Or the password is concatenated with a salt as a string.
163+
*/
164+
override predicate isSanitizer(DataFlow::Node node) {
165+
exists(MethodAccess ma |
166+
ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "System") and
167+
ma.getMethod().hasName("arraycopy") and
168+
ma.getArgument(0) = node.asExpr()
169+
) // System.arraycopy(password.getBytes(), ...)
170+
or
171+
exists(AddExpr e | hasAddExprAncestor(e, node.asExpr())) // password+salt
172+
or
173+
exists(ConditionalExpr ce | ce.getAChildExpr() = node.asExpr()) // useSalt?password+":"+salt:password
174+
or
175+
exists(MethodAccess ma |
176+
ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "StringBuilder") and
177+
ma.getMethod().hasName("append") and
178+
ma.getArgument(0) = node.asExpr() // stringBuilder.append(password).append(salt)
179+
)
180+
or
181+
exists(MethodAccess ma |
182+
ma.getQualifier().(VarAccess).getVariable().getType() instanceof Interface and
183+
ma.getAnArgument() = node.asExpr() // Method access of interface type variables requires runtime determination thus not handled
184+
)
185+
}
186+
}
187+
188+
from DataFlow::PathNode source, DataFlow::PathNode sink, HashWithoutSaltConfiguration cc
189+
where cc.hasFlowPath(source, sink)
190+
select sink, source, sink, "$@ is hashed without a salt.", source, "The password"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import java.security.NoSuchAlgorithmException;
2+
3+
public interface HASH {
4+
void init() throws NoSuchAlgorithmException;
5+
6+
int getBlockSize();
7+
8+
void update(byte[] foo, int start, int len) throws NoSuchAlgorithmException;
9+
10+
byte[] digest() throws NoSuchAlgorithmException;
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
edges
2+
| HashWithoutSalt.java:10:36:10:43 | password : String | HashWithoutSalt.java:10:36:10:54 | getBytes(...) |
3+
| HashWithoutSalt.java:25:13:25:20 | password : String | HashWithoutSalt.java:25:13:25:31 | getBytes(...) |
4+
| HashWithoutSalt.java:93:22:93:29 | password : String | HashWithoutSalt.java:94:17:94:25 | passBytes |
5+
| HashWithoutSalt.java:111:22:111:29 | password : String | HashWithoutSalt.java:112:18:112:26 | passBytes |
6+
nodes
7+
| HashWithoutSalt.java:10:36:10:43 | password : String | semmle.label | password : String |
8+
| HashWithoutSalt.java:10:36:10:54 | getBytes(...) | semmle.label | getBytes(...) |
9+
| HashWithoutSalt.java:25:13:25:20 | password : String | semmle.label | password : String |
10+
| HashWithoutSalt.java:25:13:25:31 | getBytes(...) | semmle.label | getBytes(...) |
11+
| HashWithoutSalt.java:93:22:93:29 | password : String | semmle.label | password : String |
12+
| HashWithoutSalt.java:94:17:94:25 | passBytes | semmle.label | passBytes |
13+
| HashWithoutSalt.java:111:22:111:29 | password : String | semmle.label | password : String |
14+
| HashWithoutSalt.java:112:18:112:26 | passBytes | semmle.label | passBytes |
15+
#select
16+
| HashWithoutSalt.java:10:36:10:54 | getBytes(...) | HashWithoutSalt.java:10:36:10:43 | password : String | HashWithoutSalt.java:10:36:10:54 | getBytes(...) | $@ is hashed without a salt. | HashWithoutSalt.java:10:36:10:43 | password : String | The password |
17+
| HashWithoutSalt.java:25:13:25:31 | getBytes(...) | HashWithoutSalt.java:25:13:25:20 | password : String | HashWithoutSalt.java:25:13:25:31 | getBytes(...) | $@ is hashed without a salt. | HashWithoutSalt.java:25:13:25:20 | password : String | The password |
18+
| HashWithoutSalt.java:94:17:94:25 | passBytes | HashWithoutSalt.java:93:22:93:29 | password : String | HashWithoutSalt.java:94:17:94:25 | passBytes | $@ is hashed without a salt. | HashWithoutSalt.java:93:22:93:29 | password : String | The password |
19+
| HashWithoutSalt.java:112:18:112:26 | passBytes | HashWithoutSalt.java:111:22:111:29 | password : String | HashWithoutSalt.java:112:18:112:26 | passBytes | $@ is hashed without a salt. | HashWithoutSalt.java:111:22:111:29 | password : String | The password |

0 commit comments

Comments
 (0)