🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

A Valid Email Address Ran Code on GNU Savannah. The Validator Was Right and the Shell Was the Bug

Hacktron's write-up of GNU Savannah shows a backtick in an RFC-valid email local part reaching /bin/sh through PHP proc_open, then chaining to superuser, impersonation of Bash's maintainer and root. What to check in your own code.

Contents

On GNU Savannah, an email address like 1a`touch${IFS}/tmp/savane-rce-proof`@attacker.example was accepted by the account validator and later executed as a shell command. That is the first step in Hacktron’s write-up of how it reached code execution on the forge that hosts Bash and Coreutils. The validator did its job. The bug is that an address the validator rightly accepts was later concatenated into a command line.

The code path

According to Hacktron, the vulnerable function is test_listing() in frontend/php/include/gpg.php, reached from the “Test GPG keys” button on a user’s account page. It builds a gpg command string, appends the user’s email address to the options when it wants to test a key against that address, and passes the string to utils_run_proc(), which calls proc_open(). The post quotes these lines:

$cmd = gpg_name () . " --home '$temp_dir' $command";
$options = '--list-keys --fingerprint';
if ($test_against_email) { $options .= " $email"; }

When proc_open() receives a string, PHP runs it through /bin/sh, so backticks and $() are expanded before gpg starts. The validator in account.php rejects literal spaces, which is why the payload uses ${IFS}, but it accepts backticks, $, braces, / and | in the local part. Hacktron notes that the email RFCs allow many of those characters, so the address is valid by the standard as well as by Savannah’s code.

I ran the difference locally, with PHP 8.3.6 and a harmless echo in place of gpg:

$email = '1a`echo${IFS}INJECTED`@attacker.example';

proc_open("echo --list-keys $email", ...);        // prints: --list-keys 1aINJECTED@attacker.example
proc_open(['echo', '--list-keys', $email], ...);  // prints: --list-keys 1a`echo${IFS}INJECTED`@attacker.example

The string form executed the substitution. The array form, which PHP 7.4 and later accept and which skips the shell, passed the address through as one literal argument. This reproduces the mechanism only. I did not run Savane.

Why “validate the email better” is the wrong fix

Tightening the regex would have closed this payload and left the design in place. Any later feature that interpolates another user-controlled string, such as a real name, a key comment or a project name, into the same helper has the same problem. The durable fix is at the sink: pass arguments as an array so that no input can change the shape of the command, and add -- before user values for tools that parse options. Validation is still worth having, but for correctness of the data, not as the thing standing between a user and /bin/sh.

The email case is a good reminder because the RFC grammar for the local part is wider than most people assume, and a quoted local part allows more again. Code that treats an address as “safe because it passed a validator” is depending on the validator to know every downstream context. It cannot.

The chain is where the damage came from

Per the write-up, the web-server RCE alone did not reach Bash. The next steps were ordinary:

  1. The web user could read Savane’s configuration, including database credentials, and the team set the superuser flag on its own account.
  2. Superusers have a “Become this user” feature. Hacktron used it to reach the Bash maintainer’s SSH key page and confirmed that edits were accepted. It says it did not add a key or touch the repository.
  3. The backend ran a kernel still vulnerable to GameOver(lay), CVE-2023-2640 and CVE-2023-32629 in OverlayFS. The public exploit gave root on the first attempt, and root exposed a key to a GNU backup host.

Two of these are worth auditing in your own systems. One is the impersonation feature. If your admin panel can act as any user, then database write access is equivalent to that user’s SSH keys, API tokens and deploy credentials, and the audit log has to record the real actor. The other is unpatched kernels on hosts that run unprivileged web code. A fixed Ubuntu 5.15 kernel was published on 2026-05-07, four days before Hacktron’s report on 2026-05-11, and the FSF installed it on 2026-05-14.

What limited the damage

Hacktron says a rogue commit would not have reached Bash users automatically, because releases are signed tarballs made by the maintainer, so a planted change would have had to get past him. That is the useful property here: the repository was one place an attacker could write, and the signing step was another place that they could not. Hacktron itself compares the potential to the xz backdoor, which is a claim about what could have happened and not a finding that anything did. The FSF’s June 19 statement, as summarized by Hacktron, said it had no reason to believe project data or credentials were accessed. I did not read the statement itself.

Scope of the audit

Hacktron reports 33 findings from its AI-assisted audit of Savane’s source, of which 7 were false positives and 26 were confirmed: 2 critical, 6 high, 16 medium, 2 low. Besides the command injection it lists an authorization bypass in site-wide admin group configuration, a private file read that trusted a request parameter, session hijacking through URL parameters in cross-domain SSO, reset hashes that never expire, and stored XSS through the real name field. The FSF fixed the authorization problems by refactoring rather than patching individual checks. These counts are the researchers’ own, and I have not seen an independent tally.

The FSF also warned the operator of the other public Savane instance. Savane is not unique to GNU, so anyone running it should look at the fixes published on 2026-08-07.

A short audit for your own code

  • Grep for proc_open, exec, shell_exec, system, passthru and backticks in PHP, and for shell=True, os.system and string-built subprocess calls in Python. Each hit that includes user data in a string is the same bug.
  • Prefer argument arrays. Where a tool treats a leading - as an option, add -- before user-supplied values.
  • Treat “become this user” as a credential-minting feature: restrict it, alert on it and log the original actor.
  • Keep the database credentials out of files the web user can read when a feature does not need them.

Sources