VNX-CS-009: C# use of weak cryptographic algorithm (MD5, SHA-1, DES, 3DES, RC2, RC4)
Reports C# code that creates or invokes MD5, SHA-1, DES, 3DES, RC2, RC4, MD4/MD2 or PBKDF1 through the .NET or BouncyCastle crypto APIs, including SHA-1/MD5 signatures and algorithms requested by name. HMAC, RSA-OAEP, PBKDF2, protocol-fixed hashes (WebSocket, Gravatar, ETag, Content-MD5), analyzer-suppressed code, comments, strings and test projects are not reported.
| Field | Value |
|---|---|
| Rule ID | VNX-CS-009 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | C# |
| CWE | CWE-327 |
| CAPEC | CAPEC-97 |
| ATT&CK | T1600 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | weak-crypto cryptography csharp |
| Source | internal/sast/rules/vnx-cs-009.rego |
Overview
This rule reports a line of C# (.cs) that creates or calls a broken or deprecated cryptographic primitive. It matches the API call, not the algorithm’s name, so an enum member DigestKind.MD5, a property called SHA1, OrderByDescending or a log message naming MD5.Create() is not a finding. What it reports:
- Hashes:
MD5.Create(),SHA1.Create(), the one-shotMD5.HashData(),SHA1.HashData(),HashDataAsync()andTryHashData()(also fully qualified, split across two lines, or through ausing Alias = System.Security.Cryptography.MD5;alias), and the classesMD5CryptoServiceProvider,MD5Cng,SHA1Managed,SHA1CryptoServiceProviderandSHA1Cng(new X(),new X { ... },X x = new();). - Ciphers:
DES.Create(),TripleDES.Create(),RC2.Create()andDESCryptoServiceProvider,TripleDESCryptoServiceProvider,TripleDESCng,RC2CryptoServiceProvider. - By name:
HashAlgorithm.Create("MD5"),SymmetricAlgorithm.Create("TripleDES"),CryptoConfig.CreateFromName("SHA1"),CryptoConfig.MapNameToOID("SHA1")andformatter.SetHashAlgorithm("SHA1"). - Hashing and signing with a weak
HashAlgorithmName:IncrementalHash.CreateHash(HashAlgorithmName.MD5),CryptographicOperations.HashData(HashAlgorithmName.MD5, ...),SignData/SignHash/VerifyData/VerifyHashandnew CertificateRequest(...)withHashAlgorithmName.SHA1orMD5, also when the argument sits on its own line of a multi-line call or is a variable or field the file assigns fromHashAlgorithmName.SHA1/MD5(var alg = HashAlgorithmName.SHA1;thenrsa.VerifyData(data, sig, alg, ...)). The .NET Framework overloads that name the hash as a string:rsa.SignData(data, "SHA1"),rsa.VerifyData(data, "MD5", sig). An XML signature whoseSignatureMethodorDigestMethodis set toSignedXml.XmlDsigRSASHA1Url,XmlDsigDSAUrlorXmlDsigSHA1Url. - PBKDF1:
new PasswordDeriveBytes(...). - BouncyCastle:
MD5Digest,MD4Digest,MD2Digest,Sha1Digest,DesEngine,DesEdeEngine,RC2Engine,RC4Engine, andDigestUtilities.GetDigest("MD5"),CipherUtilities.GetCipher("DES/CBC/..."),SignerUtilities.GetSigner("SHA1withRSA").
Deliberately not reported:
- HMAC constructions (
HMACSHA1,HMACMD5,IncrementalHash.CreateHMAC). HMAC does not depend on collision resistance, and TOTP and several webhook schemes fix HMAC-SHA1. - RSA-OAEP with SHA-1 (
RSAEncryptionPadding.OaepSHA1) and PBKDF2 (Rfc2898DeriveBytes), whatever its hash. - A
HashAlgorithmName.SHA1that is compared, mapped in aswitchor rejected rather than used. - A hash whose surrounding code shows a protocol-fixed, non-security use: the WebSocket
Sec-WebSocket-Acceptkey (RFC 6455), Gravatar URLs, ETags, Content-MD5 /ContentHash, and name-based GUIDs. - Code under an explicit analyzer suppression:
#pragma warning disable CA5350,CA5351orCA5373(until the matchingrestore), or a[SuppressMessage(..., "CA5351...")]on the member. - Comments, string literals, and test projects (
tests/,*.Tests/,*.UnitTests/,*Tests.cs). - ECB mode and an algorithm chosen at run time from a variable (
HashAlgorithm.Create(name)) are outside this rule.
MD5 and SHA-1 are hash functions with known collision attacks. MD5 collisions can be computed in seconds on modern hardware, and chosen-prefix collisions are practical; SHA-1 suffered its first practical chosen-prefix collision in 2020. Using either for integrity or signatures lets an attacker substitute data without changing the digest.
DES has a 56-bit key, which is exhaustively searchable in hours. RC2 and RC4 are similarly weak. TripleDES offers at most 112 bits of security, has a 64-bit block (Sweet32), is deprecated by NIST, and is slower than AES.
Why This Matters
The practical break of MD5 has had real security consequences. The Flame malware (2012) used an MD5 collision attack to forge a valid Microsoft code-signing certificate, allowing it to appear as legitimately signed Windows Update software. This allowed the malware to spread within networks that trusted Microsoft’s signing infrastructure. SHA-1 TLS certificates were deprecated by all major browsers by 2017 precisely because chosen-prefix collisions make certificate forgery practical.
For password storage, MD5 and SHA-1 are catastrophically weak: they are fast hash functions designed for speed, and modern GPUs compute billions of MD5 or SHA-1 hashes per second, reducing the time to crack a password database from years to hours even with salted hashes. Password hashing must use purpose-built slow KDFs: PBKDF2, bcrypt, scrypt, or Argon2.
In .NET these algorithms remain available for legacy interoperability, and the .NET analyzers CA5350 and CA5351 flag them. The *CryptoServiceProvider and *Managed classes are obsolete (SYSLIB0021) in current .NET. CAPEC-97 (Cryptanalysis) and ATT&CK T1600 both relate to this class of cryptographic weakness.
What Gets Flagged
// FLAGGED: MD5 for a password hash
using var md5 = MD5.Create();
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(salt + password));
// FLAGGED: one-shot SHA-1
return SHA1.HashData(Encoding.UTF8.GetBytes(token));
// FLAGGED: DES and 3DES
using var des = new DESCryptoServiceProvider();
var tdes = new TripleDESCryptoServiceProvider { Key = key, Mode = CipherMode.CBC };
// FLAGGED: algorithm requested by name
return (HashAlgorithm)CryptoConfig.CreateFromName("SHA1");
// FLAGGED: SHA-1 signature
return _rsa.SignData(payload, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
// NOT FLAGGED: HMAC-SHA1 for TOTP, SHA-1 fixed by the WebSocket protocol
using var hmac = new HMACSHA1(secret);
var accept = SHA1.HashData(Encoding.ASCII.GetBytes(clientKey + WebSocketGuid));
Remediation
- For hashing data integrity (files, documents, non-password data): use SHA-256 (
SHA256.Create()) or SHA-3 (SHA3_256.Create()in .NET 8+). - For password hashing: use
Rfc2898DeriveBytes(PBKDF2 with SHA-256) with at least 310,000 iterations, or use a third-party library providing bcrypt or Argon2. - For symmetric encryption: replace DES/3DES/RC2/RC4 with AES using GCM mode (
AesGcmclass, .NET Core 3.0+) for authenticated encryption. - For signatures and certificates: pass
HashAlgorithmName.SHA256or stronger toSignData,VerifyDataandCertificateRequest, and useSignedXml.XmlDsigRSASHA256Url/XmlDsigSHA256Urlfor XML signatures. - For new MAC designs prefer
HMACSHA256orHMACSHA512. HMAC-SHA1 is not reported, since it remains acceptable where a protocol such as TOTP fixes it.
// SAFE: SHA-256 for data integrity
using var sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(fileBytes);
// SAFE: PBKDF2 with SHA-256 for password hashing
using var pbkdf2 = new Rfc2898DeriveBytes(
password,
salt: RandomNumberGenerator.GetBytes(32),
iterations: 310_000,
hashAlgorithm: HashAlgorithmName.SHA256);
byte[] passwordHash = pbkdf2.GetBytes(32);
// SAFE: AES-GCM for authenticated encryption
byte[] key = RandomNumberGenerator.GetBytes(32);
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
using var aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag);
References
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
- OWASP – Cryptographic Storage Cheat Sheet
- Microsoft Docs – .NET Cryptography Model
- NIST SP 800-131A – Transitioning the Use of Cryptographic Algorithms and Key Lengths
- CAPEC-97: Cryptanalysis
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-cs-009
// nosec vnx-cs-009
Or record an ignore rule: vulnetix ignore add --rule vnx-cs-009 --reason "...". See Suppressing findings.