The Keystore.Passwords.GPG package of ada-keystore (and its akt CLI) builds the GPG encryption command from a template, gpg --encrypt --batch --yes -r $USER, by substituting the caller-supplied User value into $USER unescaped. The resulting string was handed to the String-form Util.Processes.Spawn, which tokenizes on bare spaces. A User value containing spaces therefore split into additional GPG argv elements (argument injection, CWE-88, leading to OS command-level impact, CWE-78).
An attacker who controls the recipient identifier (e.g. a wrapper that pipes an external username into akt password --user <X>) could inject flags such as --output to write the encrypted blob to an arbitrary path, --logger-file to write attacker-chosen files, or --keyring / --pinentry-mode loopback --passphrase=… to subvert key handling.
Affected: ada-keystore version 1.4.2 and earlier.
Fixed: ada-keystore version 1.4.3.
Saving a GPG-protected secret with a hostile recipient string (the embedded spaces break out into extra argv slots):
-- attacker-controlled recipient Save_Secret (User => "me@example.com --logger-file /tmp/akt_pwn_marker", ...);
The template gpg --encrypt --batch --yes -r $USER expands and is tokenized into:
["gpg", "--encrypt", "--batch", "--yes", "-r", "me@example.com", "--logger-file", "/tmp/akt_pwn_marker"]
GPG honours the injected --logger-file before any keyring lookup, so /tmp/akt_pwn_marker is written even though the encryption itself fails for lack of a recipient key (proving the primitive). With a valid recipient, the same lever carries --output /path to write the encrypted blob anywhere the keystore process can reach.
The weakness was surfaced by Pragmatic's Spawn checker (CWE-78), which flags subprocess-launch sinks whose arguments may derive from untrusted input. Pointed at the GPG password module, it reported every Util.Processes.Spawn call site (including the encrypt sink that receives the unescaped $USER substitution):
$ ./bin/pragmatic -E=Spawn -e=adb src/keystore-passwords-gpg.adb 78,Spawn,src/keystore-passwords-gpg.adb,257,Pragmatic Scanner,"Subprocess launch: arguments derived from untrusted input must be validated or escaped before being passed to Spawn / Expect. Prefer an absolute program path and an argument array (no shell interpolation)."
Line 257 is the Save_Secret encrypt call. Following that sink back to Get_Encrypt_Command revealed the unescaped, space-preserving $USER substitution feeding the String-form Spawn, exactly the “prefer an argument array, no shell interpolation” advice the finding recommends.
The maintainer reworked the encrypt path to split the command template into a vector of arguments before expanding $USER, then dispatch through the structured vector-of-string Util.Processes.Spawn overload. Get_Encrypt_Command now returns a Vector_String: the template is tokenized first, and the User value is appended as a single, indivisible argv element. The vector overload execs the program directly (no shell, and no space re-splitting), so a recipient string such as me@example.com --logger-file /tmp/… arrives at GPG as one (invalid) recipient argument rather than as injected flags. The injection lever is closed at the source.
Our sincere thanks to Stéphane Carrez, the maintainer of ada-keystore and a prolific contributor to the Ada ecosystem, who was a genuine pleasure to work with. He responded the very next day, engaged thoughtfully and patiently with the analysis (taking the time to clarify related information along the way), and delivered a clean, well-reasoned fix. His kindness, clarity, and obvious care for his users' security are exactly what the open-source community is at its best, and we are grateful for everything he does for Ada.
CVSS 4.0 Base Score: 8.4 (High)
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
An attacker who controls the recipient identifier reaching the GPG encrypt path injects arbitrary gpg arguments (VC:H/VI:H/VA:H). On a shared keystore deployment this is a local privilege-escalation primitive; on a service that accepts a recipient identifier from the network, re-score with AV:N for unauthenticated RCE-class impact.
6a4d489 and a11a2d1), moving the GPG encrypt path to the structured vector-of-arguments Spawn overload.