diff --git a/CHANGELOG.md b/CHANGELOG.md index d6c0cde..4055176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [Unreleased] +- Improved `weak-password-hash` regex in `crypto_usage.py` to prevent false positives when benign variables matching `password` (e.g. `passwordResetToken`, `password_hash`) are used in fast hashes. + ## [0.19.0] — 2026-09-22 Migration: **none required.** The one new surface is an opt-in flag, and nothing existing changes diff --git a/src/websec_validator/extractors/crypto_usage.py b/src/websec_validator/extractors/crypto_usage.py index e6a93ba..08f0073 100644 --- a/src/websec_validator/extractors/crypto_usage.py +++ b/src/websec_validator/extractors/crypto_usage.py @@ -23,7 +23,7 @@ from .base import Extractor, RepoContext, is_test_file from .syntax import expression_end, in_literal, js_functions, without_comments -_PW = r"(?:password|passwd|passphrase|\bpwd\b|userPassword|plainPassword)" +_PW = r"(?:password|passwd|passphrase|\bpwd\b|userPassword|plainPassword)(?![a-zA-Z0-9_]*?(?:[Tt]oken|[Hh]ash|[Ss]alt|[Aa]ttempt|[Rr]eset|[Cc]ount|[Uu]ri|[Uu]rl|[Ii]d\b|[Ff]ile))" # a fast digest fed a password-shaped value (either arg order, within a small window) WEAK_PW_HASH = re.compile( r"createHash\s*\(\s*['\"](?:md5|sha1|sha256|sha224)['\"]\s*\)[\s\S]{0,160}?\.update\s*\([^)]*" + _PW diff --git a/tests/test_pentest_regressions.py b/tests/test_pentest_regressions.py index c88fc85..6897f0d 100644 --- a/tests/test_pentest_regressions.py +++ b/tests/test_pentest_regressions.py @@ -572,6 +572,22 @@ def test_client_findings_carry_their_own_file(self): self.assertEqual(ctv["file"], "app/Pay.tsx") +class CryptoUsageFalsePositiveTests(unittest.TestCase): + def test_benign_password_variables_ignored(self): + from websec_validator.extractors.crypto_usage import CryptoUsageExtractor + ctx = repo({ + "test1.js": "createHash('sha256').update(passwordResetToken).digest('hex');", + "test2.py": "hashlib.sha256(password_hash.encode()).hexdigest()", + "test3.js": "createHash('md5').update(user.passwordAttemptCount.toString()).digest()", + "test4.js": "sha256(password_salt + user.id).digest()", + "test5.py": "hashlib.md5(password_file_path).hexdigest()", + "test6.js": "createHash('sha256').update(passwordResetUrl).digest()", + }) + res = CryptoUsageExtractor().extract(ctx, {}) + findings = res.get("findings", []) + self.assertEqual(len(findings), 0, f"Expected 0 findings, got {len(findings)}: {findings}") + + class AuthSchemeTests(unittest.TestCase): """P2: HMAC-signed cookies must not misread as api-key (which staged JWT probes for a no-JWT app)."""