|
| 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" |
0 commit comments