DEV Community

CopperSunDev
CopperSunDev

Posted on • Originally published at coppersun.dev

Weak Crypto in AI-Generated Python

BrassCoders flags weak cryptography deterministically through Bandit's crypto rules, because AI assistants produce it at a measured rate. Pearce et al. found roughly 40% of GitHub Copilot programs vulnerable across the CWE Top 25, and the FormAI study labeled 51.24% of 112,000 generated C programs vulnerable by formal verification. Weak crypto is a pattern the model inherited from a decade of tutorials that reach for MD5 and AES-ECB because they are short and they run.

The dangerous part isn't that the model knows a weak primitive. It's that the weak primitive satisfies the prompt on the input the developer tested, and the security gap only opens against an attacker the prompt never described.

The Rate AI Ships Weak Crypto

BrassCoders treats weak crypto as a high-confidence AI failure mode because three independent studies measured it. Pearce et al. (Asleep at the Keyboard?, IEEE S&P 2022) generated 1,689 programs with GitHub Copilot across 89 scenarios drawn from MITRE's CWE Top 25 and found approximately 40% vulnerable, with the CWE-327 cryptography scenarios among the categories the model handled worst.

The field data agrees. Fu et al. (Security Weaknesses of Copilot-Generated Code in GitHub Projects, ACM TOSEM 2025) studied Copilot snippets merged into real public repositories and found the single most frequent weakness was CWE-330, use of insufficiently random values. That is the randomness failure that undermines session tokens, nonces, and keys. The same study reports that feeding static-analysis warnings back through the assistant fixed up to 55.5% of the issues.

The largest measurement comes from formal verification. The FormAI dataset (Tihanyi et al. 2023) generated 112,000 C programs and labeled them with the ESBMC bounded model checker rather than a heuristic linter; 51.24% carried at least one vulnerability, crypto misuse among the labeled classes. Because the labeling is a formal proof, that rate is a floor.

What Bandit Flags

BrassCoders bundles Bandit's cryptography rules and reports every match the same way on every run: MD5 and SHA1 (B303), insecure ciphers and ECB mode (B304 and B305), weak hashlib algorithms (B324), and the standard random module used in a security context (B311). Each is a structural pattern in the source, not a judgment about the surrounding code.

A password hasher that reaches for MD5 produces a finding shaped like this:

issues:
  - id: bandit-B303-001
    severity: high
    file_path: auth/password.py
    line_number: 22
    title: Use of Weak MD5 Hash for Security
    description: MD5 is a fast, collision-prone hash unsuitable for password
      storage. An attacker who obtains the hash can brute-force it cheaply.
    remediation: Replace with a slow, salted password hash — bcrypt, scrypt,
      or Argon2 via passlib or the hashlib.scrypt primitive.
Enter fullscreen mode Exit fullscreen mode

The AI assistant reading the findings file gets the severity, the exact line, and the fix direction before it reads a line of source. It confirms the finding and writes the patch. The discovery step is already done.

The MD5 in the Corpus Is Actually Fine

BrassCoders flags the MD5 call in file_dedupe.py from its published corpus, and that finding is a false positive: the file hashes file contents to detect duplicates, a non-security use where MD5's speed is a feature and its collision-weakness doesn't matter. The generated code:

with open(path, "rb") as f:
    digest = hashlib.md5(f.read()).hexdigest()
if digest in seen:
    duplicates.append((path, seen[digest]))
Enter fullscreen mode Exit fullscreen mode

This is exactly the case where a context-inferring scanner would go wrong in the other direction. A rule that demoted MD5 findings whenever the variable was named digest or the file was named *_dedupe.py would also demote a real password hash that happened to use those names. BrassCoders refuses that trade. It reports the md5 call, and the AI assistant reading the findings decides whether this instance is security-sensitive.

That division is the point. BrassCoders is the pattern reporter; the AI consumer of the YAML is the context-aware triage layer. The false positive is a feature, not a failure, because the alternative is a scanner that stays silent on the real bug to keep its false-positive count down.

Why Review Alone Misses Weak Crypto

BrassCoders puts a deterministic check under human review because review alone is unreliable on crypto, and the strongest evidence is a controlled study. Perry et al. (Do Users Write More Insecure Code with AI Assistants?, ACM CCS 2023) ran participants through security-sensitive tasks including one that asked them to encrypt and sign a message. Those with an AI assistant wrote significantly less secure code, and were more likely to believe their code was secure than the control group.

The confidence gap is what makes this hard to catch by eye. A developer trusts weak crypto more when an assistant produced it, so it clears review with less scrutiny. A deterministic scanner underneath the review doesn't share that bias. It flags the md5 call whether a human, a model, or a coin flip put it there.

The Fixes

BrassCoders emits a remediation note with each crypto finding, and the fixes are well-defined by use case. Password hashing moves off MD5 and SHA1 onto a slow, salted algorithm:

from passlib.hash import argon2

hashed = argon2.hash(password)
argon2.verify(candidate, hashed)
Enter fullscreen mode Exit fullscreen mode

Symmetric encryption moves off ECB, which leaks structure because identical plaintext blocks produce identical ciphertext blocks, onto an authenticated mode:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

aesgcm = AESGCM(key)
ct = aesgcm.encrypt(nonce, plaintext, associated_data)
Enter fullscreen mode Exit fullscreen mode

Token and key generation moves off the random module, whose Mersenne Twister is predictable from observed outputs, onto secrets:

import secrets

token = secrets.token_hex(32)
Enter fullscreen mode Exit fullscreen mode

Each fix is the kind of well-defined replacement the model implements from the remediation note in one pass. The scanner narrows the solution space; the assistant writes the specific patch against the surrounding code.

pip install brasscoders
brasscoders --offline scan /path/to/your/project
Enter fullscreen mode Exit fullscreen mode

The scanner flags the pattern on every run. The AI assistant triages the context. You keep the crypto that's fine and fix the crypto that isn't.

Top comments (0)