{"title":"Insecure Cryptographic Key Derivation via Hardcoded Salt in PBKDF2","language":"Python","severity":"Critical","cwe":"CWE-760","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"import hashlib\nimport binascii\ndef derive_vault_key(master_pwd: str) -> str:\n    static_grain = b'az09xk11'\n    iterations = 1000\n    raw_key = hashlib.pbkdf2_hmac(\n        'sha256',\n        master_pwd.encode('utf-8'),\n        static_grain,\n        iterations,\n        dklen=32\n    )\n    return binascii.hexlify(raw_key).decode('ascii')","explanation":"The salt `static_grain` is hardcoded as a fixed byte string `b'az09xk11'`, which means every invocation of `derive_vault_key` with the same master password produces an identical derived key. A cryptographic salt must be randomly generated per-use to prevent precomputation attacks (rainbow tables). Additionally, the iteration count of 1000 is far below the recommended minimum (OWASP recommends ≥600,000 for PBKDF2-SHA256), making brute-force attacks trivially fast.","remediation":"The fix replaces the hardcoded static salt with a cryptographically secure random salt generated via `os.urandom(32)` on each invocation, ensuring every derived key is unique even for identical passwords. The iteration count is raised from 1,000 to 600,000 in line with OWASP's current recommendation for PBKDF2-SHA256, making brute-force attacks computationally expensive. The function now returns both the derived key and the salt as hex strings so the caller can persist the salt for future re-derivation, and it validates any externally supplied salt is at least 16 bytes.","secure_code":"import hashlib\nimport binascii\nimport os\n\n# Minimum iteration count per OWASP 2023 guidelines for PBKDF2-SHA256\n_PBKDF2_ITERATIONS = 600_000\n_SALT_LENGTH = 32  # 256-bit random salt\n_DKLEN = 32        # 256-bit derived key\n\n\ndef derive_vault_key(master_pwd: str, salt: bytes | None = None) -> tuple[str, str]:\n    \"\"\"Derive a vault encryption key from a master password using PBKDF2.\n\n    Args:\n        master_pwd: The master password to derive the key from.\n        salt: Optional salt bytes. If None, a cryptographically secure\n              random salt is generated. When verifying / re-deriving an\n              existing key, pass the previously stored salt.\n\n    Returns:\n        A tuple of (hex_derived_key, hex_salt). The caller MUST persist\n        the salt alongside the derived key so it can be reproduced later.\n    \"\"\"\n    if salt is None:\n        salt = os.urandom(_SALT_LENGTH)\n    elif len(salt) < 16:\n        raise ValueError(\"Provided salt must be at least 16 bytes.\")\n\n    raw_key = hashlib.pbkdf2_hmac(\n        'sha256',\n        master_pwd.encode('utf-8'),\n        salt,\n        _PBKDF2_ITERATIONS,\n        dklen=_DKLEN\n    )\n    return binascii.hexlify(raw_key).decode('ascii'), binascii.hexlify(salt).decode('ascii')"}