Hardcoded Credentials in AI-Generated Python

BrassCoders found hardcoded credentials in 2 of 15 AI-generated files: token_check.py had a literal HMAC signing key, email_sender.py had an SMTP password. Both came from realistic prompts.

Copper Sun Brass Team · · 4 min read
securityai-code-reviewbenchmarks

BrassCoders found hardcoded credentials in two of the fifteen AI-generated Python files in its published benchmark corpus: token_check.py embedded a literal HMAC signing key, and email_sender.py embedded a plain-text SMTP password. Both came from realistic prompts. Neither file was asked to handle secrets securely.

This is how AI-generated code produces credential leaks: not through carelessness, but through prompt completion.

The HMAC Key in token_check.py

BrassCoders flagged token_check.py for a hardcoded secret; the file was generated from the prompt “Write a function that signs and verifies a session token using HMAC — include a usable example so I can run it,” and the model embedded a literal signing key at module scope to make the example runnable.

The relevant line:

SECRET_KEY = "s3cr3t-signing-key-change-me"

The comment in the name (“change-me”) suggests the model knew this was a placeholder. The value is still in the source file. A developer who copies this function into their application without replacing SECRET_KEY ships a signing key that’s now in version control, in every clone of the repository, in every CI build artifact, and in every deployment. HMAC security depends entirely on the key remaining secret; a leaked key lets an attacker forge valid session tokens.

Yelp’s detect-secrets — the upstream library BrassCoders uses for secret detection — flags high-entropy strings at module scope. The custom BrassCoders secret-pattern scanner adds pattern matching for common credential variable names. Both fire on SECRET_KEY = "s3cr3t-signing-key-change-me".

The SMTP Password in email_sender.py

BrassCoders flagged email_sender.py for a hardcoded password; the file was generated from the prompt “Write a function that sends a templated welcome email to a user over SMTP,” and the model wrote a working SMTP login with a literal password.

The relevant lines:

with smtplib.SMTP("smtp.example.com", 587) as server:
    server.starttls()
    server.login("noreply@example.com", "hunter2-mailpassword")
    server.send_message(msg)

"hunter2-mailpassword" is the literal second argument to server.login(). It’s in the source file, passed directly to the SMTP server. A developer who uses this function in production ships their SMTP credentials to every environment that gets the code. Credential rotation means changing the source file and redeploying, not rotating the credential in a secrets manager.

Bandit’s B106 rule flags hardcoded values passed to function arguments that match credential-related parameter names. BrassCoders surfaces this alongside detect-secrets’ entropy analysis, so the finding appears from two scanners in the same output.

Why the Model Includes the Credential

The prompt asked for “a usable example so I can run it.” A usable HMAC example requires a key. A usable SMTP example requires credentials. The model fills in values that make the example execute without errors on a local machine. The instruction to use environment variables or a secrets manager isn’t in the prompt, so the model doesn’t add it.

This is the prompt-completion dynamic that produces credentials in code. The model isn’t generating insecure code because it doesn’t know better — it’s generating code that satisfies the stated requirement. A prompt that explicitly asks for a secrets-manager integration produces that instead. Most prompts don’t.

A Veracode 2025 analysis of AI-generated code found 45% of AI-generated code had at least one OWASP Top 10 vulnerability; credential exposure is among the most common classes.

The Fix

Both findings have the same fix: move the credential to an environment variable.

For token_check.py:

import os

SECRET_KEY = os.environ["HMAC_SECRET_KEY"]

The key no longer lives in the source file. It’s injected at runtime from the environment. Version control doesn’t contain it; build artifacts don’t contain it; the key can be rotated without a code change.

For email_sender.py:

smtp_password = os.environ["SMTP_PASSWORD"]
server.login("noreply@example.com", smtp_password)

BrassCoders emits the remediation note with each finding. Claude Code reads “Replace hardcoded credential with an environment variable read” and generates the os.environ version from the flagged line.

Reproducing the Findings

git clone https://github.com/CopperSunDev/brasscoders
pip install brasscoders
brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files

The hardcoded-credential findings for token_check.py and email_sender.py appear in .brass/ai_instructions.yaml with severity high, file path, line number, and the environment-variable remediation. Same output on every run.

Frequently Asked Questions

Why do AI coding assistants put credentials in code?

AI assistants complete code to satisfy the prompt. A prompt for an HMAC signing function includes a working example with a key; a prompt for an email-sending function includes a working SMTP login. The model fills in values that make the example run. The credential is a placeholder, but it's in the code, and the structural pattern is indistinguishable from a real credential to a scanner or a future reader.

Does BrassCoders detect all secret types?

BrassCoders detects 20+ secret formats using Yelp's detect-secrets as the upstream library plus custom patterns. These include AWS access keys, GitHub personal access tokens, Stripe live keys, OpenAI keys, Slack tokens, PEM-formatted private keys, JWTs, and high-entropy strings. Patterns outside this rule set require the AI triage layer.

Will BrassCoders flag placeholder credentials in test fixtures?

BrassCoders' own test fixtures use FIXTURE-marked placeholder values (AKIAFIXTURE000000000, ghp_FIXTUREbcdef...) to exercise the detection logic without triggering production alerts. Real credentials in production code don't carry a FIXTURE marker. The scanner matches on the structural pattern, not the content; a FIXTURE-marked placeholder reduces false-positive noise in the test suite.

Can I reproduce these findings from the published corpus?

Yes. Clone the BrassCoders OSS repo, install brasscoders, and run brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files. The hardcoded-credential findings for token_check.py and email_sender.py appear in the output.