From HIGH to Clean: Removing a Hardcoded HMAC Key in AI-Generated Python
A real before-and-after: BrassCoders's SecretsScanner finds a hardcoded HMAC signing key in a corpus Python file. The environment variable fix, the re-scan, and why detect-secrets catches this where Bandit alone misses some cases.
The model wrote what you asked for. The prompt said “include a usable example so I can run it” — and the model gave you one, complete with SECRET_KEY = "s3cr3t-signing-key-change-me" hardcoded in source. BrassCoders flags that assignment as HIGH severity on the first scan: confidence 0.85, impact_score 0.9, secret_type “Secret Keyword.”
Two lines of code fix it. Here’s the full walkthrough, with real scan output.
The Starting Point: The Prompt Asked for a Runnable Example
BrassCoders’s published corpus includes token_check.py, 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.” The phrase “Include a usable example” is the instruction that produces the credential — the model needs a key to sign with, so it fills one in.
This pattern recurs across AI-generated auth and signing code. The demo needs a working value. The model produces a placeholder-looking string, the code runs, the developer commits it. "s3cr3t-signing-key-change-me" reads like a reminder to swap it out — but it’s still a hardcoded literal in version-controlled source until someone does.
A placeholder key with “change-me” in the name signals intent without enforcing it. No runtime error fires. Deploys succeed. HMAC signatures verify correctly in tests. The production deploy looks identical to the local demo. There’s nothing to alert on until someone checks whether the key is actually loaded from a safe source.
The file that came out of the model:
import hmac
import hashlib
SECRET_KEY = "s3cr3t-signing-key-change-me"
def sign(payload):
mac = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256)
return mac.hexdigest()
def verify(payload, signature):
expected = sign(payload)
return hmac.compare_digest(expected, signature)
if __name__ == "__main__":
sig = sign("user=42")
print(verify("user=42", sig))
The HMAC logic is correct. hmac.compare_digest is the right comparison function — timing-safe, no short-circuit on mismatch. The bug isn’t the signing implementation. It’s the key source.
Running the Scan: What BrassCoders Surfaces
BrassCoders’s SecretsScanner — built on detect-secrets, the upstream credential-pattern library from Yelp — catches the SECRET_KEY assignment at line 11 with HIGH severity. The finding title is “Possible hardcoded credential” and the exact value is redacted in the .brass/ai_instructions.yaml output, so the findings file doesn’t re-expose the secret.
The scan command:
brasscoders scan /path/to/corpus/
The finding in the YAML output:
- title: "Possible hardcoded credential (value redacted)"
file: token_check.py
line: 11
detector: SecretsScanner (detect-secrets)
description: >
Variable assignment: SECRET_KEY. A credential-shaped literal was
detected at this location. The exact value has been redacted to
prevent the brass output from re-exposing it.
confidence: 0.85
impact_score: 0.9
secret_type: "Secret Keyword"
also_detected_by: bandit (B105)
remediation: >
Confirm whether this value is a real credential. If yes: rotate it
immediately, remove it from history, and load it from a secrets
manager or environment variable.
Notice what’s absent: no code_snippet field. BrassCoders redacts credential-shaped values from the findings file by design. The output points to the file and line number — enough to locate and fix the credential — without reproducing the string in the YAML. If the findings file contained the literal, the findings file would be another place the secret lives.
The severity classification comes from two signals. The detect-secrets “Secret Keyword” pattern scores based on variable name: SECRET_KEY is a high-confidence credential signal. The impact_score of 0.9 reflects what this credential class controls — an HMAC signing key governs session token validity, so a compromise breaks the authentication boundary entirely, not just leaks a piece of data.
The also_detected_by: bandit (B105) field shows Bandit fired on this file too. B105 is Bandit’s generic hardcoded-password-string rule, which triggers on obviously-named credential assignments. In this case, both tools fire. The SecretsScanner is the primary signal because detect-secrets also applies entropy analysis, covering cases where the variable name is less obvious but the value’s entropy is high enough to classify as a credential. B105 alone would miss those.
The Fix: Load From Environment
BrassCoders’s remediation note for the credential finding is direct: rotate if real, then load from a secrets manager or environment variable. For token_check.py, os.environ.get("HMAC_SECRET_KEY", "") replaces the literal with a runtime-loaded value. The key never appears in source, in git history, or in build artifacts once the commit is cleaned.
The fix is two lines added to the import block:
import os
SECRET_KEY = os.environ.get("HMAC_SECRET_KEY", "")
The empty-string default lets the module import cleanly. Add a startup guard if you want the process to fail fast on a missing key rather than at signing time:
if not SECRET_KEY:
raise RuntimeError("HMAC_SECRET_KEY not set")
One step matters before removing the literal from source: check whether the key was ever deployed. If "s3cr3t-signing-key-change-me" reached production, any party with the source can forge tokens signed with it. Rotate first. Then clean the history. For git history removal, git filter-repo is the current recommended tool — it rewrites history, which requires a force push and coordination with anyone who has cloned the repo. The safer path, when the key was never deployed, is to make the environment-variable change in a new commit and leave the history in place.
.brassignore suppresses the BrassCoders finding. It does not remove the string from past commits. That’s not the right tool here.
The full fixed file:
import hmac
import hashlib
import os
SECRET_KEY = os.environ.get("HMAC_SECRET_KEY", "")
def sign(payload):
mac = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256)
return mac.hexdigest()
def verify(payload, signature):
expected = sign(payload)
return hmac.compare_digest(expected, signature)
if __name__ == "__main__":
sig = sign("user=42")
print(verify("user=42", sig))
The Re-Scan: Zero Credential Findings
BrassCoders’s re-scan of the fixed file shows SecretsScanner and Bandit B105 both silent — the credential-shaped literal is no longer present in source, so neither tool fires. The only remaining findings are Pylint code quality items for missing docstrings: zero security findings, zero HIGH or CRITICAL severity.
The scanner summary after the fix:
Secrets detection: 0 findings ✓
Bandit (security): 0 credential findings
Pylint (quality): 2 findings (missing docstrings — code quality, not security)
Critical issues: 0
High issues: 0
The Pylint warnings are legitimate. The functions have no docstrings, and Pylint will note that. Worth addressing in production code — but these are in a different category from the credential finding, and the security surface is clean.
Two credential findings before the fix. Zero after. The Pylint items were always present; SecretsScanner took priority in the first output.
What BrassCoders Doesn’t Verify
BrassCoders catches the presence of a credential-shaped literal — it cannot determine whether the value in source was ever used to sign real session tokens in production. That determination belongs to the incident response workflow: check deployment history, determine the exposure window, decide whether to rotate.
Detection time is when you know to start checking. If "s3cr3t-signing-key-change-me" was copied directly into a production deploy, any party who reads the source can verify or forge tokens signed with it. The scan tells you to look; it doesn’t tell you what was looked at before you scanned.
BrassCoders catches 20+ secret formats through detect-secrets plus 7 custom patterns — AWS access keys, GitHub PATs, Stripe live keys, OpenAI API keys, Slack tokens, PEM-formatted private keys, JWTs, and high-entropy strings, among others. The HMAC key here triggers the “Secret Keyword” pattern: variable name contains a credential-signal word, assigned a string literal. The same pattern fires on API_KEY, TOKEN, PASSWORD, and similar assignments across a codebase.
Run brasscoders scan before you commit. The scan takes seconds. The credential investigation, after the fact, takes longer.
Frequently Asked Questions
Does BrassCoders's SecretsScanner catch HMAC keys specifically?
Yes. detect-secrets — the upstream library BrassCoders uses for secret-pattern scanning — identifies SECRET_KEY assignments with credential-shaped string values as a 'Secret Keyword' pattern. BrassCoders adds 7 custom patterns on top of detect-secrets's detection set. The HMAC key in token_check.py is caught at confidence 0.85 with impact_score 0.9.
Why does the findings file not show the actual secret value?
BrassCoders redacts credential-shaped values from .brass/ai_instructions.yaml to prevent the findings file from re-exposing the secret. The finding references the file path and line number — enough to locate and fix the credential — without reproducing the literal in the YAML output. This matches how BrassCoders handles the Phase 0 security principle: never let the output surface what the input contained.
What if the key 's3cr3t-signing-key-change-me' was never deployed?
If the literal never reached production, removing it from source eliminates the risk. If it did reach production, rotate it before removal — tokens signed with the original key can be re-signed by anyone with the source. The conservative approach is always to rotate when a credential-shaped value appears in version-controlled source.
How do I set HMAC_SECRET_KEY in a production environment?
For container deployments, set it as an environment variable in the container runtime (Kubernetes Secret, ECS task definition, Docker Compose secrets). For serverless, use the platform's secrets management (AWS Secrets Manager + Lambda environment, Vercel environment variables). For local development, a .env file loaded by python-dotenv is conventional — with .env in .gitignore.
Does BrassCoders detect secrets in environment variable files?
Yes, if .env files are within the scan path. The offline scan covers all files in the target directory, including .env files the scanner can read. If your project loads secrets via dotenv and .env is not in .gitignore, BrassCoders will flag credentials it finds there. The recommended fix is .gitignore, plus environment injection at deploy time.