From 854c92dc1701cd48dff498359f52b90bf5a17dac Mon Sep 17 00:00:00 2001 From: "Michal J. Gajda" Date: Sun, 19 Apr 2026 16:56:06 +0200 Subject: [PATCH] Add 'security' hint group (enabled by default): weak hashes, read on untrusted input data/hlint.yaml: new group 'security', enabled: true. Rules: read x -> Text.Read.readMaybe x side: not (isLitString x) CWE-502 reads x -> Text.Read.readMaybe x side: not (isLitString x) CWE-502 Crypto.Hash.MD5.hash -> Crypto.Hash.SHA256.hash CWE-327 Crypto.Hash.MD5.hashlazy -> Crypto.Hash.SHA256.hashlazy CWE-327 Crypto.Hash.SHA1.hash -> Crypto.Hash.SHA256.hash CWE-327 Crypto.Hash.SHA1.hashlazy -> Crypto.Hash.SHA256.hashlazy CWE-327 Data.Digest.Pure.MD5.md5 -> Crypto.Hash.SHA256.hashlazy CWE-327 Data.Digest.Pure.SHA.sha1 -> Data.Digest.Pure.SHA.sha256 CWE-327 Disable per-rule via `-i ""` or suppress the whole group with `- group: {name: security, enabled: false}` in .hlint.yaml. tests/security.test: four cases. 1. security-hash.hs : all six hash rules fire. 2. security-read-nonliteral.hs: `read x` fires for variable `x`. 3. security-read-literal.hs : `read "42"` and `read "3.14"` do not fire. 4. security-ignored.hs : per-rule -i suppression yields no hints. Self-test: 974 tests, 3 failures (all pre-existing on master: issue #1674 intercalate/OverloadedStrings and two record-pattern type-application cases from PR #1680). Running hlint with the new group on hlint's own source produces one finding: src/Test/InputOutput.hs:57 `read code` (parsing an EXIT line from a .test file). No HSEC advisory on github.com/haskell/security-advisories maps to CWE-327 or CWE-502 for these function classes. --- data/hlint.yaml | 34 +++++++++ tests/security.test | 164 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 tests/security.test diff --git a/data/hlint.yaml b/data/hlint.yaml index f007f2b2..334689dd 100644 --- a/data/hlint.yaml +++ b/data/hlint.yaml @@ -1476,6 +1476,40 @@ - warn: {lhs: foldr' f c (reverse x), rhs: foldl' (flip f) c x, name: Use left fold instead of right fold} - warn: {lhs: foldl' f c (reverse x), rhs: foldr (flip f) c x, note: IncreasesLaziness, name: Use right fold instead of left fold} +- group: + # Security hints, enabled by default. Disable per-rule with + # `- ignore: {name: "..."}` in .hlint.yaml, or suppress the whole + # group with `- group: {name: security, enabled: false}`. + # + # Rule categories: + # * `read` -> `readMaybe` on non-literal arguments (CWE-502) + # * broken / weak hash primitives: MD5, SHA-1 (CWE-327) + name: security + enabled: true + rules: + + # `read` throws a runtime exception on malformed input; it is the + # Haskell analogue of Python's `pickle.load` / `ast.literal_eval` on + # untrusted data. Replace with `readMaybe` from `Text.Read` and + # handle `Nothing` explicitly. Literal arguments (e.g. `read "42"`) + # are not flagged because the failure is a programmer bug caught on + # first run, not a runtime attack surface. + - warn: {lhs: read x, rhs: Text.Read.readMaybe x, side: not (isLitString x), name: "Use readMaybe on untrusted input (CWE-502)"} + - warn: {lhs: reads x, rhs: Text.Read.readMaybe x, side: not (isLitString x), name: "Use readMaybe on untrusted input (CWE-502)"} + + # Broken or weak cryptographic hashes. MD5 and SHA-1 are + # cryptographically broken (MD5: chosen-prefix collisions since 2008; + # SHA-1: SHAttered, 2017) and must not be used for any security + # purpose (authentication, integrity, digital signatures). Use + # SHA-256 or stronger from `Crypto.Hash` (cryptonite/crypton); for + # password hashing use `Crypto.KDF.BCrypt` or `Crypto.KDF.Argon2`. + - warn: {lhs: Crypto.Hash.MD5.hash x, rhs: Crypto.Hash.SHA256.hash x, name: "Avoid broken hash MD5 (CWE-327)"} + - warn: {lhs: Crypto.Hash.MD5.hashlazy x, rhs: Crypto.Hash.SHA256.hashlazy x, name: "Avoid broken hash MD5 (CWE-327)"} + - warn: {lhs: Crypto.Hash.SHA1.hash x, rhs: Crypto.Hash.SHA256.hash x, name: "Avoid broken hash SHA-1 (CWE-327)"} + - warn: {lhs: Crypto.Hash.SHA1.hashlazy x, rhs: Crypto.Hash.SHA256.hashlazy x, name: "Avoid broken hash SHA-1 (CWE-327)"} + - warn: {lhs: Data.Digest.Pure.MD5.md5 x, rhs: Crypto.Hash.SHA256.hashlazy x, name: "Avoid broken hash MD5 (CWE-327)"} + - warn: {lhs: Data.Digest.Pure.SHA.sha1 x, rhs: Data.Digest.Pure.SHA.sha256 x, name: "Avoid broken hash SHA-1 (CWE-327)"} + - group: # used for tests, enabled when testing this file name: testing diff --git a/tests/security.test b/tests/security.test new file mode 100644 index 00000000..c678f074 --- /dev/null +++ b/tests/security.test @@ -0,0 +1,164 @@ +--------------------------------------------------------------------- +RUN tests/security-hash.hs --hint=data/hlint.yaml +FILE tests/security-hash.hs +module Sample where + +import qualified Crypto.Hash.MD5 as MD5 +import qualified Crypto.Hash.SHA1 as SHA1 +import qualified Data.Digest.Pure.MD5 as PMD5 +import qualified Data.Digest.Pure.SHA as PSHA +import qualified Data.ByteString as BS + +input :: BS.ByteString +input = BS.empty + +viaMD5, viaMD5Lazy, viaSHA1, viaSHA1Lazy :: BS.ByteString +viaMD5 = MD5.hash input +viaMD5Lazy = MD5.hashlazy undefined +viaSHA1 = SHA1.hash input +viaSHA1Lazy = SHA1.hashlazy undefined + +viaPureMD5 = PMD5.md5 undefined +viaPureSHA1 = PSHA.sha1 undefined +OUTPUT +tests/security-hash.hs:13:10-23: Warning: Avoid broken hash MD5 (CWE-327) +Found: + MD5.hash input +Perhaps: + Crypto.Hash.SHA256.hash input + +tests/security-hash.hs:13:10-23: Warning: Avoid broken hash MD5 (CWE-327) +Found: + MD5.hash input +Perhaps: + Crypto.Hash.SHA256.hash input + +tests/security-hash.hs:14:14-35: Warning: Avoid broken hash MD5 (CWE-327) +Found: + MD5.hashlazy undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:14:14-35: Warning: Avoid broken hash MD5 (CWE-327) +Found: + MD5.hashlazy undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:15:11-25: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + SHA1.hash input +Perhaps: + Crypto.Hash.SHA256.hash input + +tests/security-hash.hs:15:11-25: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + SHA1.hash input +Perhaps: + Crypto.Hash.SHA256.hash input + +tests/security-hash.hs:16:15-37: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + SHA1.hashlazy undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:16:15-37: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + SHA1.hashlazy undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:18:14-31: Warning: Avoid broken hash MD5 (CWE-327) +Found: + PMD5.md5 undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:18:14-31: Warning: Avoid broken hash MD5 (CWE-327) +Found: + PMD5.md5 undefined +Perhaps: + Crypto.Hash.SHA256.hashlazy undefined + +tests/security-hash.hs:19:15-33: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + PSHA.sha1 undefined +Perhaps: + PSHA.sha256 undefined + +tests/security-hash.hs:19:15-33: Warning: Avoid broken hash SHA-1 (CWE-327) +Found: + PSHA.sha1 undefined +Perhaps: + PSHA.sha256 undefined + +12 hints + +--------------------------------------------------------------------- +RUN tests/security-read-nonliteral.hs --hint=data/hlint.yaml +FILE tests/security-read-nonliteral.hs +module Sample where + +parseStr :: String -> Int +parseStr s = 1 + read s + +parseOther :: String -> Double +parseOther raw = 0.5 * read raw +OUTPUT +tests/security-read-nonliteral.hs:4:18-23: Warning: Use readMaybe on untrusted input (CWE-502) +Found: + read s +Perhaps: + Text.Read.readMaybe s + +tests/security-read-nonliteral.hs:4:18-23: Warning: Use readMaybe on untrusted input (CWE-502) +Found: + read s +Perhaps: + Text.Read.readMaybe s + +tests/security-read-nonliteral.hs:7:24-31: Warning: Use readMaybe on untrusted input (CWE-502) +Found: + read raw +Perhaps: + Text.Read.readMaybe raw + +tests/security-read-nonliteral.hs:7:24-31: Warning: Use readMaybe on untrusted input (CWE-502) +Found: + read raw +Perhaps: + Text.Read.readMaybe raw + +4 hints + + +--------------------------------------------------------------------- +RUN tests/security-read-literal.hs --hint=data/hlint.yaml +FILE tests/security-read-literal.hs +module Sample where + +answer :: Int +answer = read "42" + +piApprox :: Double +piApprox = read "3.14" +OUTPUT +No hints + + +--------------------------------------------------------------------- +RUN tests/security-ignored.hs --hint=data/hlint.yaml -i "Avoid broken hash MD5 (CWE-327)" -i "Use readMaybe on untrusted input (CWE-502)" +FILE tests/security-ignored.hs +module Sample where + +import qualified Crypto.Hash.MD5 as MD5 +import qualified Data.ByteString as BS + +digest :: BS.ByteString +digest = MD5.hash BS.empty + +parseStr :: String -> Int +parseStr s = 1 + read s +OUTPUT +No hints