Review secrets compared with non-constant-time equality
Matches methods that compare strings or byte sequences with String.Equals(), ==, String.Compare() or SequenceEqual() and either compute a hash / MAC / derived key in the same body, or carry a name mentioning a token, signature, HMAC, secret, OTP, password, API key, digest or nonce. Ordinary equality short-circuits on the first mismatch, so the elapsed time leaks how many leading bytes were right: a timing side channel that has been exploited over networks.
This is CWE-208 Observable Timing Discrepancy.
Flagged Code
This is CWE-208 Observable Timing Discrepancy.
Flagged Code
bool VerifyToken(string token, byte[] key, byte[] payload) {
using var hmac = new HMACSHA256(key);
string expected = Convert.ToHexString(hmac.ComputeHash(payload));
return expected == token; // flagged: == returns as soon as a byte differs
}
Fixed Codebool VerifyToken(string token, byte[] key, byte[] payload) {
using var hmac = new HMACSHA256(key);
byte[] expected = hmac.ComputeHash(payload);
byte[] provided = Convert.FromHexString(token);
return CryptographicOperations.FixedTimeEquals(expected, provided);
}How to fix
Compare secrets with System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(a, b) on the binary form of both values. For two strings, compare their UTF-16 bytes — MemoryMarshal.AsBytes(a.AsSpan()) vs MemoryMarshal.AsBytes(b.AsSpan()) — which is the exact ordinal equality. Do NOT re-encode them to UTF-8 first: an unpaired surrogate becomes U+FFFD, so two different strings can encode to the same bytes and compare equal. For passwords, compare the stored hash with the freshly computed hash the same way. Suppress the issue when the compared values are not secrets.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review secrets compared with non-constant-time equality</Name>
// <Id>ND3113:ReviewSecretsComparedWithNonConstantTimeEquality</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let equalityMembers = ThirdParty.Types.WithFullNameIn(
"System.String", "System.StringComparer", "System.Linq.Enumerable", "System.MemoryExtensions")
.ChildMethods().Where(m => m.SimpleNameEqualsAny("Equals", "op_Equality", "op_Inequality", "Compare", "CompareOrdinal", "SequenceEqual"))
.ToHashSetEx()
// Computing a MAC, hash or derived key and then comparing it is the strongest signal.
let hashComputingMembers = ThirdParty.Methods
.Where(m => m.ParentNamespace.Name == "System.Security.Cryptography" &&
m.SimpleNameEqualsAny("ComputeHash", "TryComputeHash", "HashData", "TryHashData", "GetBytes", "Pbkdf2", "GetHashAndReset"))
.ToHashSetEx()
let constantTimeEquals = ThirdParty.Types.WithFullName("System.Security.Cryptography.CryptographicOperations")
.ChildMethods().Where(m => m.SimpleName == "FixedTimeEquals").ToHashSetEx()
where equalityMembers.Any()
from m in Application.Methods.UsingAny(equalityMembers)
where !m.MethodsCalled.Intersect(constantTimeEquals).Any()
let comparesComputedHash = m.MethodsCalled.Intersect(hashComputingMembers).Any()
// A name like VerifyToken(), ValidateSignature(), IsPasswordValid(), RedeemPasswordResetPin() or CheckApiKey()
let nameSuggestsSecret =
m.SimpleName.ContainsAny(new[] { "Token", "Signature", "Hmac", "Secret", "Otp", "Password", "Passwd", "ApiKey", "Digest", "Nonce", "Csrf", "Antiforgery" }, StringComparison.OrdinalIgnoreCase) &&
m.SimpleName.ContainsAny(new[] { "Verify", "Validate", "Valid", "Check", "Compare", "Match", "Authenticate", "Redeem", "Confirm", "Equal" }, StringComparison.OrdinalIgnoreCase) &&
!m.SimpleName.ContainsAny(new[] { "Cancellation", "Tokenize", "Tokenizer", "PublicKeyToken", "HelpToken" }, StringComparison.OrdinalIgnoreCase)
where comparesComputedHash || nameSuggestsSecret
select new {
m,
equalityMembersUsed = m.MethodsCalled.Intersect(equalityMembers).ToArray(),
comparesComputedHash,
Debt = 10.ToMinutes().ToDebt(),
Severity = comparesComputedHash ? Severity.High : Severity.Medium
}
//<Expl>
// The *{0}* {2?computes a hash or MAC and compares it:has a name suggesting it checks a secret}
// with *{1}*, which returns as soon as the first byte differs. An attacker
// measuring response times can recover the expected value byte by byte.
//</Expl>
//<Desc>
// Matches methods that compare strings or byte sequences with
// *String.Equals()*, *==*, *String.Compare()* or *SequenceEqual()* and
// either compute a hash / MAC / derived key in the same body, or carry a
// name mentioning a token, signature, HMAC, secret, OTP, password, API key,
// digest or nonce. Ordinary equality short-circuits on the first mismatch,
// so the elapsed time leaks how many leading bytes were right: a timing
// side channel that has been exploited over networks.
//
// This is CWE-208 *Observable Timing Discrepancy*.
//
// **Flagged Code**
// ```csharp
// bool VerifyToken(string token, byte[] key, byte[] payload) {
// using var hmac = new HMACSHA256(key);
// string expected = Convert.ToHexString(hmac.ComputeHash(payload));
// return expected == token; // flagged: == returns as soon as a byte differs
// }
// ```
//
// **Fixed Code**
// ```csharp
// bool VerifyToken(string token, byte[] key, byte[] payload) {
// using var hmac = new HMACSHA256(key);
// byte[] expected = hmac.ComputeHash(payload);
// byte[] provided = Convert.FromHexString(token);
// return CryptographicOperations.FixedTimeEquals(expected, provided);
// }
// ```
//</Desc>
//<HowToFix>
// Compare secrets with
// *System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(a, b)*
// on the binary form of both values. For two strings, compare their UTF-16
// bytes — *MemoryMarshal.AsBytes(a.AsSpan())* vs *MemoryMarshal.AsBytes(b.AsSpan())*
// — which is the exact ordinal equality. Do NOT re-encode them to UTF-8 first:
// an unpaired surrogate becomes U+FFFD, so two different strings can encode
// to the same bytes and compare equal. For passwords, compare the stored hash
// with the freshly computed hash the same way. Suppress the issue when the
// compared values are not secrets.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138