Skip to content

Commit 07f45a5

Browse files
committed
Query to detect hash without salt
1 parent 1c8547c commit 07f45a5

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// semmle-extractor-options: /r:System.Security.Cryptography.Primitives.dll /r:System.Security.Cryptography.Csp.dll /r:System.Security.Cryptography.Algorithms.dll
2+
3+
using System;
4+
using System.Security.Cryptography;
5+
6+
using Windows.Security.Cryptography;
7+
using Windows.Security.Cryptography.Core;
8+
using Windows.Storage.Streams;
9+
10+
public class Test
11+
{
12+
private const int SaltSize = 32;
13+
14+
// BAD - Hash without a salt.
15+
public static String HashPassword(string password, string strAlgName ="SHA256")
16+
{
17+
IBuffer passBuff = CryptographicBuffer.ConvertStringToBinary(password, BinaryStringEncoding.Utf8);
18+
HashAlgorithmProvider algProvider = HashAlgorithmProvider.OpenAlgorithm(strAlgName);
19+
IBuffer hashBuff = algProvider.HashData(passBuff);
20+
return CryptographicBuffer.EncodeToBase64String(hashBuff);
21+
}
22+
23+
// GOOD - Hash with a salt.
24+
public static string HashPassword2(string password, string salt, string strAlgName ="SHA256")
25+
{
26+
// Concatenate the salt with the password.
27+
IBuffer passBuff = CryptographicBuffer.ConvertStringToBinary(password+salt, BinaryStringEncoding.Utf8);
28+
HashAlgorithmProvider algProvider = HashAlgorithmProvider.OpenAlgorithm(strAlgName);
29+
IBuffer hashBuff = algProvider.HashData(passBuff);
30+
return CryptographicBuffer.EncodeToBase64String(hashBuff);
31+
}
32+
33+
// BAD - Hash without a salt.
34+
public static string HashPassword(string password)
35+
{
36+
SHA256 sha256Hash = SHA256.Create();
37+
byte[] passBytes = System.Text.Encoding.ASCII.GetBytes(password);
38+
byte[] hashBytes = sha256Hash.ComputeHash(passBytes);
39+
return Convert.ToBase64String(hashBytes);
40+
}
41+
42+
// GOOD - Hash with a salt.
43+
public static string HashPassword2(string password)
44+
{
45+
byte[] passBytes = System.Text.Encoding.ASCII.GetBytes(password);
46+
byte[] saltBytes = GenerateSalt();
47+
48+
// Add the salt to the hash.
49+
byte[] rawSalted = new byte[passBytes.Length + saltBytes.Length];
50+
passBytes.CopyTo(rawSalted, 0);
51+
saltBytes.CopyTo(rawSalted, passBytes.Length);
52+
53+
//Create the salted hash.
54+
SHA256 sha256 = SHA256.Create();
55+
byte[] saltedPassBytes = sha256.ComputeHash(rawSalted);
56+
57+
// Add the salt value to the salted hash.
58+
byte[] dbPassword = new byte[saltedPassBytes.Length + saltBytes.Length];
59+
saltedPassBytes.CopyTo(dbPassword, 0);
60+
saltBytes.CopyTo(dbPassword, saltedPassBytes.Length);
61+
62+
return Convert.ToBase64String(dbPassword);
63+
}
64+
65+
public static byte[] GenerateSalt()
66+
{
67+
using (var rng = new RNGCryptoServiceProvider())
68+
{
69+
var randomNumber = new byte[SaltSize];
70+
rng.GetBytes(randomNumber);
71+
return randomNumber;
72+
}
73+
}
74+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
edges
2+
| HashWithoutSalt.cs:17:70:17:77 | access to parameter password : String | HashWithoutSalt.cs:19:49:19:56 | access to local variable passBuff |
3+
| HashWithoutSalt.cs:37:28:37:72 | call to method GetBytes : Byte[] | HashWithoutSalt.cs:38:51:38:59 | access to local variable passBytes |
4+
| HashWithoutSalt.cs:37:64:37:71 | access to parameter password : String | HashWithoutSalt.cs:37:28:37:72 | call to method GetBytes : Byte[] |
5+
nodes
6+
| HashWithoutSalt.cs:17:70:17:77 | access to parameter password : String | semmle.label | access to parameter password : String |
7+
| HashWithoutSalt.cs:19:49:19:56 | access to local variable passBuff | semmle.label | access to local variable passBuff |
8+
| HashWithoutSalt.cs:37:28:37:72 | call to method GetBytes : Byte[] | semmle.label | call to method GetBytes : Byte[] |
9+
| HashWithoutSalt.cs:37:64:37:71 | access to parameter password : String | semmle.label | access to parameter password : String |
10+
| HashWithoutSalt.cs:38:51:38:59 | access to local variable passBytes | semmle.label | access to local variable passBytes |
11+
#select
12+
| HashWithoutSalt.cs:19:49:19:56 | access to local variable passBuff | HashWithoutSalt.cs:17:70:17:77 | access to parameter password : String | HashWithoutSalt.cs:19:49:19:56 | access to local variable passBuff | $@ is hashed without a salt. | HashWithoutSalt.cs:17:70:17:77 | access to parameter password | The password |
13+
| HashWithoutSalt.cs:38:51:38:59 | access to local variable passBytes | HashWithoutSalt.cs:37:64:37:71 | access to parameter password : String | HashWithoutSalt.cs:38:51:38:59 | access to local variable passBytes | $@ is hashed without a salt. | HashWithoutSalt.cs:37:64:37:71 | access to parameter password | The password |
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' cases, no salt is provided. In the 'GOOD' cases, a salt is provided.</p>
16+
<sample src="HashWithoutSalt.cs" />
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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
* @id cs/hash-without-salt
6+
* @tags security
7+
* external/cwe-759
8+
*/
9+
10+
import csharp
11+
import semmle.code.csharp.dataflow.TaintTracking
12+
import DataFlow::PathGraph
13+
14+
/** The C# class `System.Security.Cryptography.SHA...` other than the weak `SHA1`. */
15+
class SHA extends RefType {
16+
SHA() { this.getQualifiedName().regexpMatch("System\\.Security\\.Cryptography\\.SHA\\d{2,3}") }
17+
}
18+
19+
class HashAlgorithmProvider extends RefType {
20+
HashAlgorithmProvider() {
21+
this.hasQualifiedName("Windows.Security.Cryptography.Core", "HashAlgorithmProvider")
22+
}
23+
}
24+
25+
/** The method call `ComputeHash()` declared in `System.Security.Cryptography.SHA...`. */
26+
class ComputeHashMethodCall extends MethodCall {
27+
ComputeHashMethodCall() {
28+
this.getQualifier().getType() instanceof SHA and
29+
this.getTarget().hasName("ComputeHash")
30+
}
31+
}
32+
33+
/** The method call `ComputeHash()` declared in `System.Security.Cryptography.SHA...`. */
34+
class HashDataMethodCall extends MethodCall {
35+
HashDataMethodCall() {
36+
this.getQualifier().getType() instanceof HashAlgorithmProvider and
37+
this.getTarget().hasName("HashData")
38+
}
39+
}
40+
41+
/** Gets a regular expression for matching common names of variables that indicate the value being held is a password. */
42+
string getPasswordRegex() { result = "(?i).*pass(wd|word|code|phrase).*" }
43+
44+
/** Finds variables that hold password information judging by their names. */
45+
class PasswordVarExpr extends Expr {
46+
PasswordVarExpr() {
47+
exists(Variable v | this = v.getAnAccess() | v.getName().regexpMatch(getPasswordRegex()))
48+
}
49+
}
50+
51+
/** Taint configuration tracking flow from an expression whose name suggests it holds password data to a method call that generates a hash without a salt. */
52+
class HashWithoutSaltConfiguration extends TaintTracking::Configuration {
53+
HashWithoutSaltConfiguration() { this = "HashWithoutSaltConfiguration" }
54+
55+
override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof PasswordVarExpr }
56+
57+
override predicate isSink(DataFlow::Node sink) {
58+
exists(ComputeHashMethodCall mc |
59+
sink.asExpr() = mc.getArgument(0) // sha256Hash.ComputeHash(rawDatabytes)
60+
) or
61+
exists(HashDataMethodCall mc |
62+
sink.asExpr() = mc.getArgument(0) // algProv.HashData(rawDatabytes)
63+
)
64+
}
65+
66+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
67+
exists(MethodCall mc |
68+
mc.getTarget()
69+
.hasQualifiedName("Windows.Security.Cryptography.CryptographicBuffer",
70+
"ConvertStringToBinary") and
71+
mc.getArgument(0) = node1.asExpr() and
72+
mc = node2.asExpr()
73+
)
74+
}
75+
76+
/**
77+
* Holds if a password is concatenated with a salt then hashed together through the call `System.Array.CopyTo()`, for example,
78+
* `byte[] rawSalted = new byte[passBytes.Length + salt.Length];`
79+
* `passBytes.CopyTo(rawSalted, 0);`
80+
* `salt.CopyTo(rawSalted, passBytes.Length);`
81+
* `byte[] saltedPassword = sha256.ComputeHash(rawSalted);`
82+
* Or the password is concatenated with a salt as a string.
83+
*/
84+
override predicate isSanitizer(DataFlow::Node node) {
85+
exists(MethodCall mc |
86+
mc.getTarget().fromLibrary() and
87+
mc.getTarget().hasQualifiedName("System.Array", "CopyTo") and
88+
mc.getArgument(0) = node.asExpr()
89+
) // passBytes.CopyTo(rawSalted, 0)
90+
or
91+
exists(AddExpr e | node.asExpr() = e.getAnOperand()) // password+salt
92+
}
93+
}
94+
95+
from DataFlow::PathNode source, DataFlow::PathNode sink, HashWithoutSaltConfiguration c
96+
where c.hasFlowPath(source, sink)
97+
select sink.getNode(), source, sink, "$@ is hashed without a salt.", source.getNode(),
98+
"The password"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace Windows.Security.Cryptography
2+
{
3+
public enum BinaryStringEncoding
4+
{
5+
Utf8,
6+
Utf16LE,
7+
Utf16BE
8+
}
9+
10+
public static class CryptographicBuffer
11+
{
12+
public static Windows.Storage.Streams.IBuffer ConvertStringToBinary(string value, BinaryStringEncoding encoding) => throw null;
13+
14+
public static string EncodeToBase64String(Windows.Storage.Streams.IBuffer buffer) => throw null;
15+
}
16+
}
17+
18+
namespace Windows.Storage.Streams
19+
{
20+
public interface IBuffer {
21+
public uint Capacity { get; }
22+
23+
public uint Length { get; set; }
24+
}
25+
}
26+
27+
namespace Windows.Security.Cryptography.Core
28+
{
29+
public sealed class CryptographicKey { }
30+
31+
public sealed class SymmetricKeyAlgorithmProvider
32+
{
33+
public CryptographicKey CreateSymmetricKey(Windows.Storage.Streams.IBuffer keyMaterial) => throw null;
34+
}
35+
36+
public sealed class HashAlgorithmProvider {
37+
public string AlgorithmName { get; }
38+
39+
public uint HashLength { get; }
40+
41+
public static HashAlgorithmProvider OpenAlgorithm(string algorithm) => throw null;
42+
43+
public Windows.Storage.Streams.IBuffer HashData(Windows.Storage.Streams.IBuffer data) => throw null;
44+
}
45+
}

0 commit comments

Comments
 (0)