Feb 27, 2026·7 min read·7 visits
The 'badkeys' tool, used to identify weak cryptographic keys, trusts input too much. Attackers can inject invisible terminal commands into key metadata. When a researcher scans a malicious key, the tool prints the warning, but the injected commands immediately clear the screen and reprint a "Safe" message. It's a Jedi Mind Trick for your terminal.
A classic terminal escape sequence injection vulnerability in 'badkeys' allows attackers to manipulate scan results. By embedding ANSI control characters in cryptographic metadata (like DKIM records or SSH comments), an attacker can overwrite critical security warnings with harmless-looking text, effectively blinding the auditor.
There is a profound irony in security tools that are, themselves, insecure. We rely on utilities like badkeys to act as the gatekeepers, scanning our cryptographic infrastructure for weak keys, compromised moduli, and known exploits. We trust the output implicitly. If the tool says "RED ALERT," we panic. If it says nothing, we sleep soundly.
But what if the key itself could talk back? CVE-2026-21439 is a vulnerability in badkeys (versions <= 0.0.15) that turns the scanner into a liar. It's not a remote code execution bug that will pop a shell on your box (probably). It's more subtle and, in some ways, more insidious. It allows a malicious cryptographic key to manipulate the terminal of the person scanning it.
This is a Terminal Escape Sequence Injection. It's an ancient class of bug, dating back to the days of physical teletypes, that refuses to die. By embedding specific byte sequences in data that the tool naively prints to the screen, an attacker can control the cursor, change colors, or clear lines. In the context of a security audit, this means an attacker can make a critically vulnerable key look perfectly safe, effectively gaslighting the auditor.
The root cause here is a failure to sanitize "metadata" before shoving it into stdout or stderr. The badkeys tool is designed to parse complex structures: DKIM DNS records, SSH public keys, and JSON Web Keys (JWKs). These structures often contain human-readable fields—comments in SSH keys, key IDs in JWKs, or raw parameter data in DKIM records.
The developers made a fatal assumption: that these fields were just text. In reality, they are byte streams controlled by the adversary. When badkeys encounters a problematic key, it wants to be helpful. It constructs a warning message effectively saying, "Hey, check out this value: [INSERT VALUE HERE]."
Here is the logic flow of the failure:
If that interpolated value contains \x1b[2K (ANSI escape for "Clear Line") followed by \r (Carriage Return), the terminal executes those commands. The tool technically prints the warning, but the terminal deletes it faster than the human eye can register, replacing it with whatever text the attacker provided next. It’s the digital equivalent of someone whispering a warning in your ear while simultaneously setting off an airhorn.
Let's look at the code. In Python, f-strings are great, but they don't sanitize control characters by default. The vulnerability existed across multiple modules where external data was printed.
The Vulnerable Pattern (Before):
In badkeys/checks.py (and similar files), the code would take a value found in a DNS record or key file and print it directly:
# Vulnerable implementation
if potential_issue:
sys.stderr.write(f"Warning: potentially dangerous value detected: {user_input}\n")If user_input is \x1b[31mEVIL\x1b[0m, your terminal prints "EVIL" in red. If it's \x1b[2K\rAll systems operational., your terminal clears the line and prints "All systems operational."
The Fix (After):
The fix, implemented in commit 635a2f3b1b50a895d8b09ec8629efc06189f349a, introduces a sanitizer function called _esc. This function wraps Python's built-in repr(), which is designed to return a printable representation of an object, escaping non-printable characters.
# In badkeys/utils.py
def _esc(inp):
# repr() returns strings wrapped in quotes (e.g., "'foo'")
# The slice [1:-1] removes the quotes but keeps the escaping.
return repr(inp)[1:-1]
# In usage:
sys.stderr.write(f"Warning: value: {_esc(user_input)}\n")By running the input through _esc, a malicious byte \x1b becomes the literal string \\x1b. The terminal renders the slash and the x, rather than interpreting the Escape command. It defangs the payload completely.
Let's construct a Proof of Concept (PoC) that demonstrates the danger. Imagine you are a sysadmin scanning a list of SSH keys provided by a vendor. You trust badkeys to flag the weak ones.
The Setup: I, the attacker, give you an Ed25519 key. It's perfectly valid crypto-wise, but I've stuffed the "comment" field with ANSI garbage.
The Payload:
We want to hide the filename and the fact that badkeys might flag something (or just confuse the user). We will use:
\x1b[2K: Clear the entire current line.\r: Return cursor to the start of the line.[+] Scan Complete. 0 Vulnerabilities found. : A fake success message.# Creating the malicious SSH key file
# Note: This is a "valid" format for SSH tools, even if the comment is garbage.
echo -e 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdNlqItIkvAGtuRUJFHfUTM2RyaQaEUMAEBF9UsWSQO key\x1b[2K\r[+] Scan Complete. 0 Vulnerabilities found.' > vulnerable_keys.pub
# The Victim runs the scan
badkeys --ssh-lines vulnerable_keys.pubThe Result:
Without the exploit, badkeys prints the line being scanned. With the exploit, the output flashes and is immediately overwritten by our fake success message. The auditor sees a green light where there should be a red flag. If this key actually was weak (e.g., the "Debian weak key" vulnerability), the warning about it would be obliterated from the screen before the user could read it.
You might be thinking, "So what? It's just text on a screen. It's not Root." And technically, you're right. This isn't RCE (unless you are using a very old, very broken terminal emulator that allows command execution via escape codes, which is rare these days).
However, in the world of security auditing, Integrity is everything. If I can compromise the reporting mechanism, I don't need to compromise the scanner.
Consider these scenarios:
badkeys in CI/CD. The logs (which capture stdout) capture the manipulated text. The PR passes because the logs say "0 Vulnerabilities".This vulnerability attacks the "Human Layer" (Layer 8) of the OSI model. It exploits the trust the user places in their tools.
The remediation is straightforward, but it requires discipline. If you are using badkeys, update to version 0.0.16 immediately. The developers have patched the holes by enforcing repr()-style escaping on all external inputs.
If you are a developer writing CLI tools, take this as a lesson:
> [!NOTE]
> Never trust stdout.
rich library or similar tools often handle this better than raw print() statements, or at least offer utilities to strip control codes.cat -v or less which will escape or display control characters explicitly, revealing the attack attempt.# Workaround for older versions
badkeys --dkim malicious_record | cat -vThis will render the escape sequences as visible text (e.g., ^[[31m) rather than executing them.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
badkeys badkeys | <= 0.0.15 | 0.0.16 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-150 |
| CVSS v3.1 | 5.3 (Medium) |
| CVSS v4.0 | 2.0 (Low) |
| Attack Vector | Network / Local (Context Dependent) |
| Impact | Integrity (Log Spoofing / UI Manipulation) |
| Fix Version | 0.0.16 |
Improper Neutralization of Escape, Meta, or Control Sequences