CVE-2026-44709
Description
pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.8.7, pamusb-pinentry reads the PINENTRY_FALLBACK_APP environment variable and executes it directly without any validation. Any process that can set environment variables before pamusb-pinentry is invoked can point PINENTRY_FALLBACK_APP at an arbitrary binary or script and have it executed with the privileges of the pam_usb tool chain. This vulnerability is fixed in 0.8.7.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
pam_usb prior to 0.8.7 allows arbitrary command execution via the PINENTRY_FALLBACK_APP environment variable in pamusb-pinentry.
Vulnerability
In pam_usb versions prior to 0.8.7, the pamusb-pinentry tool reads the PINENTRY_FALLBACK_APP environment variable and passes it directly to subprocess.run() without any validation [1]. This allows an attacker who can control environment variables before pamusb-pinentry is invoked to execute an arbitrary binary or script with the privileges of the pam_usb tool chain. The affected component is tools/pamusb-pinentry.
Exploitation
An attacker needs the ability to set environment variables in the context where pamusb-pinentry is executed. This could be achieved by a local user or a process that can influence the environment (e.g., via a shell or a PAM configuration). The attacker sets PINENTRY_FALLBACK_APP to the path of a malicious executable. When pamusb-pinentry runs (e.g., during PAM authentication), it executes the attacker-controlled binary without any checks [1].
Impact
Successful exploitation grants the attacker arbitrary code execution with the privileges of the pamusb-pinentry process. Depending on the system configuration, this could lead to privilege escalation or full system compromise if the pam_usb tool chain runs with elevated permissions [1].
Mitigation
The vulnerability is fixed in pam_usb version 0.8.7. The fix validates that PINENTRY_FALLBACK_APP is an absolute path to an existing executable file; if validation fails, the tool exits with code 1 instead of executing the value [1]. Users should upgrade to version 0.8.7 or later. No workaround is documented.
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
2014042451850[Security] Fix multiple advisories of various crititicallity - UPDATE ASAP!
8 files changed · +170 −39
src/conf.c+1 −1 modified@@ -216,7 +216,7 @@ int pusb_conf_parse( continue; } - strcpy(opts->device_list[currentDevice].name, device_list[currentDevice]); + snprintf(opts->device_list[currentDevice].name, sizeof(opts->device_list[currentDevice].name), "%s", device_list[currentDevice]); pusb_conf_parse_device(opts, doc, currentDevice, device_list[currentDevice]); }
src/device.c+15 −3 modified@@ -50,16 +50,28 @@ static int pusb_device_connected(t_pusb_options *opts, UDisksClient *udisks) if (udisks_object_peek_drive(object)) { drive = udisks_object_get_drive(object); - retval = strcmp(udisks_drive_get_serial(drive), opts->device_list[currentDevice].serial) == 0; + + const gchar *serial = udisks_drive_get_serial(drive); + if (!serial) + { + log_debug("Drive has no serial number, skipping.\n"); + g_object_unref(drive); + continue; + } + retval = strcmp(serial, opts->device_list[currentDevice].serial) == 0; if (strcmp(opts->device_list[currentDevice].vendor, "Generic") != 0) { - retval = retval && strcmp(udisks_drive_get_vendor(drive), opts->device_list[currentDevice].vendor) == 0; + const gchar *vendor = udisks_drive_get_vendor(drive); + retval = retval && vendor != NULL && + strcmp(vendor, opts->device_list[currentDevice].vendor) == 0; } if (strcmp(opts->device_list[currentDevice].model, "Generic") != 0) { - retval = retval && strcmp(udisks_drive_get_model(drive), opts->device_list[currentDevice].model) == 0; + const gchar *model = udisks_drive_get_model(drive); + retval = retval && model != NULL && + strcmp(model, opts->device_list[currentDevice].model) == 0; } g_object_unref(drive);
src/pad.c+52 −20 modified@@ -43,7 +43,7 @@ static int pusb_pad_build_device_path( struct stat sb; snprintf(path_devpad, sizeof(path_devpad), "%s/%s", mnt_point, opts->device_pad_directory); - if (stat(path_devpad, &sb) != 0) + if (lstat(path_devpad, &sb) != 0) { log_debug("Directory %s does not exist, creating it.\n", path_devpad); if (mkdir(path_devpad, S_IRUSR | S_IWUSR | S_IXUSR) != 0) @@ -52,6 +52,11 @@ static int pusb_pad_build_device_path( return 0; } } + else if (S_ISLNK(sb.st_mode)) + { + log_error("Device pad directory %s is a symlink, refusing to use it.\n", path_devpad); + return 0; + } snprintf( path_out, @@ -91,7 +96,7 @@ static int pusb_pad_build_system_path( user_ent->pw_dir, opts->system_pad_directory ); - if (stat(dir_path, &sb) != 0) + if (lstat(dir_path, &sb) != 0) { log_debug("Directory %s does not exist, creating one.\n", dir_path); if (mkdir(dir_path, S_IRUSR | S_IWUSR | S_IXUSR) != 0) @@ -107,6 +112,11 @@ static int pusb_pad_build_system_path( chmod(dir_path, S_IRUSR | S_IWUSR | S_IXUSR); } + else if (S_ISLNK(sb.st_mode)) + { + log_error("System pad directory %s is a symlink, refusing to use it.\n", dir_path); + return 0; + } /* change slashes in device name to underscores */ snprintf(device_name, sizeof(device_name), "%s", opts->device.name); while (*device_name_ptr) @@ -137,19 +147,26 @@ static FILE *pusb_pad_open_device( const char *mode ) { - FILE *f; char path[1024*5]; + int flags = (mode[0] == 'r') ? O_RDONLY : (O_WRONLY | O_CREAT | O_TRUNC); if (!pusb_pad_build_device_path(opts, mnt_point, user, path, sizeof(path))) { return NULL; } - f = fopen(path, mode); - if (!f) + int fd = open(path, flags | O_NOFOLLOW, 0600); + if (fd < 0) { log_debug("Cannot open device file: %s\n", strerror(errno)); return NULL; } + FILE *f = fdopen(fd, mode); + if (!f) + { + close(fd); + log_debug("Cannot fdopen device file: %s\n", strerror(errno)); + return NULL; + } return f; } @@ -159,19 +176,26 @@ static FILE *pusb_pad_open_system( const char *mode ) { - FILE *f; char path[1024*5]; + int flags = (mode[0] == 'r') ? O_RDONLY : (O_WRONLY | O_CREAT | O_TRUNC); if (!pusb_pad_build_system_path(opts, user, path, sizeof(path))) { return NULL; } - f = fopen(path, mode); - if (!f) + int fd = open(path, flags | O_NOFOLLOW, 0600); + if (fd < 0) { log_debug("Cannot open system file: %s\n", strerror(errno)); return NULL; } + FILE *f = fdopen(fd, mode); + if (!f) + { + close(fd); + log_debug("Cannot fdopen system file: %s\n", strerror(errno)); + return NULL; + } return f; } @@ -295,19 +319,27 @@ static int pusb_pad_update( generateRandom(magic, sizeof(magic)); - if (!(f_device = fopen(path_device_tmp, "w"))) { - log_error("Unable to create temp device pad: %s\n", strerror(errno)); - return 0; + int fd_dev = open(path_device_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600); + if (fd_dev < 0 || !(f_device = fdopen(fd_dev, "w"))) + { + if (fd_dev >= 0) close(fd_dev); + log_error("Unable to create temp device pad: %s\n", strerror(errno)); + return 0; + } } pusb_pad_protect(user, fileno(f_device)); - if (!(f_system = fopen(path_system_tmp, "w"))) { - log_error("Unable to create temp system pad: %s\n", strerror(errno)); - fclose(f_device); - unlink(path_device_tmp); - return 0; + int fd_sys = open(path_system_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600); + if (fd_sys < 0 || !(f_system = fdopen(fd_sys, "w"))) + { + if (fd_sys >= 0) close(fd_sys); + log_error("Unable to create temp system pad: %s\n", strerror(errno)); + fclose(f_device); + unlink(path_device_tmp); + return 0; + } } pusb_pad_protect(user, fileno(f_system)); @@ -403,19 +435,19 @@ static int pusb_pad_compare( } log_debug("Loading device pad...\n"); bytes_read = fread(magic_device, sizeof(char), sizeof(magic_device), f_device); - if (!bytes_read) + if (bytes_read != sizeof(magic_device)) { - log_error("Can't read device pad!\n"); + log_error("Device pad is incomplete (%zu/%zu bytes).\n", bytes_read, sizeof(magic_device)); fclose(f_system); fclose(f_device); return 0; } log_debug("Loading system pad...\n"); bytes_read = fread(magic_system, sizeof(char), sizeof(magic_system), f_system); - if (!bytes_read) + if (bytes_read != sizeof(magic_system)) { - log_error("Can't read system pad!\n"); + log_error("System pad is incomplete (%zu/%zu bytes).\n", bytes_read, sizeof(magic_system)); fclose(f_system); fclose(f_device); return 0;
src/tmux.c+63 −5 modified@@ -18,11 +18,52 @@ #include <stdio.h> #include <string.h> #include <stdlib.h> +#include <ctype.h> #include <regex.h> #include "log.h" #include "process.h" #include "mem.h" +/* Reject characters that could break out of shell double-quotes or inject commands. */ +static int pusb_tmux_is_safe_socket_path(const char *path) +{ + if (!path || *path == '\0') return 0; + for (const char *p = path; *p; p++) { + switch (*p) { + case '"': case '\'': case '`': case '$': case '\\': + case '\n': case '\r': case '\t': case '!': case ';': + case '&': case '|': case '<': case '>': case '(': + case ')': case '{': case '}': + return 0; + } + } + return 1; +} + +/* Client ID from TMUX must be purely numeric. */ +static int pusb_tmux_is_numeric_id(const char *s) +{ + if (!s || *s == '\0') return 0; + for (; *s; s++) { + if (!isdigit((unsigned char)*s)) return 0; + } + return 1; +} + +/* Escape ERE metacharacters in username so it matches literally in the regex. */ +static void pusb_tmux_escape_for_regex(const char *src, char *dst, size_t dstlen) +{ + const char *metachar = "\\.^$*+?{}[]|()"; + size_t j = 0; + for (size_t i = 0; src[i] && j + 2 < dstlen; i++) { + if (strchr(metachar, (int)src[i])) { + dst[j++] = '\\'; + } + dst[j++] = src[i]; + } + dst[j] = '\0'; +} + char *pusb_tmux_get_client_tty(pid_t env_pid) { char *tmux_details = getenv("TMUX"); @@ -49,6 +90,18 @@ char *pusb_tmux_get_client_tty(pid_t env_pid) char *tmux_socket_path = strtok(tmux_details, ","); log_debug(" Got tmux_socket_path: %s\n", tmux_socket_path); + if (!pusb_tmux_is_safe_socket_path(tmux_socket_path)) + { + log_error("TMUX socket path contains invalid characters, denying.\n"); + return NULL; + } + + if (!pusb_tmux_is_numeric_id(tmux_client_id)) + { + log_error("TMUX client ID is not numeric, denying.\n"); + return NULL; + } + size_t get_tmux_session_details_cmd_len = strlen(tmux_socket_path) + strlen(tmux_client_id) + 64; char *get_tmux_session_details_cmd = xmalloc(get_tmux_session_details_cmd_len); snprintf(get_tmux_session_details_cmd, get_tmux_session_details_cmd_len, "LC_ALL=C; tmux -S \"%s\" list-clients -t \"\\$%s\"", tmux_socket_path, tmux_client_id); @@ -82,8 +135,9 @@ char *pusb_tmux_get_client_tty(pid_t env_pid) log_debug(" Closing pipe for 'tmux list-clients' failed, this is quite a wtf...\n"); } - char *result = xmalloc(strlen(tmux_client_tty) + 1); - strcpy(result, tmux_client_tty); + size_t tty_len = strlen(tmux_client_tty); + char *result = xmalloc(tty_len + 1); + memcpy(result, tmux_client_tty, tty_len + 1); return result; } else @@ -107,24 +161,28 @@ int pusb_tmux_has_remote_clients(const char* username) char regex_raw[BUFSIZ]; char buf[BUFSIZ]; char msgbuf[100]; - char regex_tpl[2][BUFSIZ] = { + const char *regex_tpl[2] = { "(.+)([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})(.+)tmux(.+)", //v4 "(.+)([0-9A-Fa-f]{1,4}):([0-9A-Fa-f]{1,4}):([0-9A-Fa-f]{1,4}):([0-9A-Fa-f]{1,4})(.+)tmux(.+)" // v6 }; // ... yes, these allow invalid addresses. No, I don't care. This isn't about validation but detecting remote access. Good enough ¯\_(ツ)_/¯ + /* Max Linux username is 32 chars; doubled for escaping + null terminator = 65 bytes. */ + char escaped_username[65] = {0}; + pusb_tmux_escape_for_regex(username, escaped_username, sizeof(escaped_username)); + for (int i = 0; i <= 1; i++) { log_debug(" Checking for IPv%d connections...\n", (4 + (i * 2))); - if ((fp = popen("LC_ALL=C; w", "r")) == NULL) + if ((fp = popen("LC_ALL=C; /usr/bin/w", "r")) == NULL) { log_error("tmux detected, but couldn't get `w`. Denying since remote check for tmux impossible without it!\n"); return -1; } while (fgets(buf, BUFSIZ, fp) != NULL) { - snprintf(regex_raw, BUFSIZ, "%s%s", username, regex_tpl[i]); + snprintf(regex_raw, BUFSIZ, "%s%s", escaped_username, regex_tpl[i]); status = regcomp(®ex, regex_raw, REG_EXTENDED); if (status)
tools/pamusb-agent+14 −4 modified@@ -194,13 +194,22 @@ def userDeviceThread(user): else: logger.error('Ignoring empty command for user "%s".' % userName) + _DANGEROUS_ENV_VARS = { + 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT', 'LD_DEBUG', + 'GLIBC_TUNABLES', 'PYTHONPATH', 'PERL5LIB', 'RUBYLIB', + 'IFS', 'CDPATH', 'ENV', 'BASH_ENV', + } + for henv in hotplug.findall('env'): if henv.text is not None: henv_var = re.sub(r'^(.*?)=.*$', '\\1', henv.text) henv_arg = re.sub(r'^.*?=(.*)$', '\\1', henv.text) if henv_var != '' and henv_arg != '': - henvs[henv_var] = henv_arg + if henv_var in _DANGEROUS_ENV_VARS: + logger.error('Rejecting dangerous env var "%s" for user "%s".' % (henv_var, userName)) + else: + henvs[henv_var] = henv_arg else: logger.error('Ignoring invalid command environment variable for user "%s".' % userName) else: @@ -269,9 +278,10 @@ def userDeviceThread(user): logger.info('Device "%s" has been inserted. Performing verification...' % deviceName) - cmdLine = "%s --debug --config=%s --service=pamusb-agent %s" % (options['check'], options['configFile'], userName) - logger.info('Executing "%s"' % cmdLine) - if not os.system(cmdLine): + checkArgs = [options['check'], '--debug', '--config=' + options['configFile'], '--service=pamusb-agent', userName] + logger.info('Executing "%s"' % ' '.join(checkArgs)) + checkResult = subprocess.run(checkArgs, capture_output=True) + if checkResult.returncode == 0: logger.info('Authentication succeeded. Unlocking user "%s"...' % userName) for l in events['unlock']:
tools/pamusb-conf+11 −1 modified@@ -17,6 +17,7 @@ import sys import os +import re import gi import subprocess import socket @@ -352,14 +353,23 @@ def resetPads(): if user.getAttribute('id') == options['resetPads']: # Device specific pad for user userDevice = user.getElementsByTagName('device')[0].firstChild.nodeValue + if '/' in userDevice or '..' in userDevice: + print('Invalid device ID in config (path traversal): %s' % userDevice) + sys.exit(1) padFiles.append('/home/%s/.pamusb/%s.pad' % (options['resetPads'], userDevice)) # User specific pad for device for details in devicesObj: if details.getAttribute('id') == userDevice: deviceUUID = details.getElementsByTagName('volume_uuid')[0].firstChild.nodeValue + if not re.match(r'^[0-9a-fA-F\-]+$', deviceUUID): + print('Invalid device UUID in config: %s' % deviceUUID) + sys.exit(1) try: - deviceMountPoint = subprocess.check_output(['mount | grep `readlink -f /dev/disk/by-uuid/%s` | awk \'{print $3}\'' % (deviceUUID)], shell=True) + deviceMountPoint = subprocess.check_output( + ['findmnt', '-n', '-o', 'TARGET', '--source', '/dev/disk/by-uuid/' + deviceUUID], + stderr=subprocess.DEVNULL + ) padFiles.append('%s/.pamusb/%s.%s.pad' % (deviceMountPoint.decode().strip(), options['resetPads'], socket.gethostname())) except subprocess.CalledProcessError as err: print('Could not get device mountpoint: ', err)
tools/pamusb-keyring-unlock-gnome+8 −4 modified@@ -39,11 +39,15 @@ else logger -p local0.notice -t ${0##*/}[$$] Killed existing gnome-keyring-instance. fi -# Read UNLOCK_PASSWORD from $KEYFILE -. ~/.pamusb/.keyring_unlock_password +# Read UNLOCK_PASSWORD from $KEYFILE via grep to avoid sourcing arbitrary shell +UNLOCK_PASSWORD=$(grep -oP '^UNLOCK_PASSWORD=\K.*' ~/.pamusb/.keyring_unlock_password) +if [ -z "$UNLOCK_PASSWORD" ]; then + logger -p local0.error -t ${0##*/}[$$] Could not read UNLOCK_PASSWORD from file. + exit 1 +fi -# Perform unlock -echo -n $UNLOCK_PASSWORD | gnome-keyring-daemon --daemonize --login \ +# Perform unlock — pass via stdin pipe so the password never appears as a process argument +printf '%s' "$UNLOCK_PASSWORD" | gnome-keyring-daemon --daemonize --login \ && gnome-keyring-daemon --start > /dev/null 2>&1; UNLOCKED=$? \ && logger -p local0.notice -t ${0##*/}[$$] Keyring unlocked. \ || logger -p local0.error -t ${0##*/}[$$] Keyring unlock failed.
tools/pamusb-pinentry+6 −1 modified@@ -16,6 +16,7 @@ # Street, Fifth Floor, Boston, MA 02110-1301 USA. import os +import sys import subprocess import getpass from dotenv import load_dotenv @@ -35,4 +36,8 @@ if (isAuthenticated.returncode == 0): exit() print("OK") else: - subprocess.run(fallbackPinentryApp) + if fallbackPinentryApp and os.path.isabs(fallbackPinentryApp) and os.path.isfile(fallbackPinentryApp) and os.access(fallbackPinentryApp, os.X_OK): + subprocess.run([fallbackPinentryApp]) + else: + sys.stderr.write('PINENTRY_FALLBACK_APP is not set or not a valid executable path, cannot fall back.\n') + sys.exit(1)
987f4f6ddf0aPrepare v0.8.7
7 files changed · +46 −7
arch_linux/PKGBUILD_git+1 −1 modified@@ -2,7 +2,7 @@ # Contributor: Pekka Helenius <fincer89 [at] hotmail [dot] com> pkgname=pam_usb-git -pkgver=0.8.6_r589.g370655a +pkgver=0.8.7_r596.g9bce707 pkgrel=1 pkgdesc='Hardware authentication for Linux using ordinary flash media (USB & Card based).' arch=($CARCH)
arch_linux/PKGBUILD_stable+1 −1 modified@@ -2,7 +2,7 @@ # Contributor: Pekka Helenius <fincer89 [at] hotmail [dot] com> pkgname=pam_usb -pkgver=0.8.6 +pkgver=0.8.7 pkgrel=1 pkgdesc='Hardware authentication for Linux using ordinary flash media (USB & Card based).' arch=($CARCH)
ChangeLog+10 −0 modified@@ -1,3 +1,13 @@ +* 0.8.7 + [Enhancement] Specify a dedicated device for superuser services + [Enhancement] Restore PolicyKit support (also for 127) + [Enhancement] Remove default installation of pamusb-pinentry + [Security] Fixed GHSA-822m-whrh-vrj8 + [Security] Fixed GHSA-jgv5-w6rm-7wxg + [Security] Fixed GHSA-fjpm-p9pj-mp34 + [Security] Fixed GHSA-j8cq-2gv6-gfwf + [Security] Fixed GHSA-jxrj-q67x-wr4c + * 0.8.6 [Enhancement] Documentation updates [Enhancement] Remote VSCode tunnels are now detected in deny_remote check (thx @jaoppb)
debian/changelog+12 −0 modified@@ -1,3 +1,15 @@ +libpam-usb (0.8.7) unstable; urgency=critical + * [Enhancement] Specify a dedicated device for superuser services + * [Enhancement] Restore PolicyKit support (also for 127) + * [Enhancement] Remove default installation of pamusb-pinentry + * [Security] Fixed GHSA-822m-whrh-vrj8 + * [Security] Fixed GHSA-jgv5-w6rm-7wxg + * [Security] Fixed GHSA-fjpm-p9pj-mp34 + * [Security] Fixed GHSA-j8cq-2gv6-gfwf + * [Security] Fixed GHSA-jxrj-q67x-wr4c + + -- Tobias Bäumer <tobiasbaeumer@gmail.com> Tue, 05 May 2026 21:00:00 +0200 + libpam-usb (0.8.6) unstable; urgency=medium * [Enhancement] Documentation updates * [Enhancement] Remote VSCode tunnels are now detected in deny_remote check (thx @jaoppb)
fedora/SPECS/pam_usb.spec+12 −1 modified@@ -1,7 +1,7 @@ %define _topdir /usr/local/src/pam_usb/fedora %define name pam_usb %define release 1 -%define version 0.8.6 +%define version 0.8.7 %define buildroot %{_topdir}/%{name}‑%{version}‑root BuildRoot: %{buildroot} @@ -61,6 +61,17 @@ rm -rf %{buildroot}/usr/share/pam-configs %doc %attr(0644,root,root) /usr/share/doc/pam_usb/TROUBLESHOOTING %changelog + +* Tue May 05 2026 Tobias Bäumer <tobiasbaeumer@gmail.com> - 0.8.7 +- [Enhancement] Specify a dedicated device for superuser services +- [Enhancement] Restore PolicyKit support (also for 127) +- [Enhancement] Remove default installation of pamusb-pinentry +- [Security] Fixed GHSA-822m-whrh-vrj8 +- [Security] Fixed GHSA-jgv5-w6rm-7wxg +- [Security] Fixed GHSA-fjpm-p9pj-mp34 +- [Security] Fixed GHSA-j8cq-2gv6-gfwf +- [Security] Fixed GHSA-jxrj-q67x-wr4c + * Fri May 03 2026 Tobias Bäumer <tobiasbaeumer@gmail.com> - 0.8.6 - [Enhancement] Documentation updates - [Enhancement] Remote VSCode tunnels are now detected in deny_remote check (thx @jaoppb)
SECURITY.md+9 −3 modified@@ -5,10 +5,16 @@ At every point in time only the most recent release is supported. | Version | Supported | -| ------- | ------------------ | -| 0.8.6 | :white_check_mark: | -| < 0.8.6 | :x: | +|---------| ------------------ | +| 0.8.7 | :white_check_mark: | +| < 0.8.7 | :x: | ## Reporting a Vulnerability Please email me at tobiasbaeumer@gmail.com, since Github issues are public. + +## Note on versions <= 0.8.6 + +There are multiple not yet published security advisories for these versions. Details will follow. + +But if you're running anything except 0.8.7 please update immediately!
src/version.h+1 −1 modified@@ -18,6 +18,6 @@ #ifndef PUSB_VERSION_H_ # define PUSB_VERSION_H_ -# define PUSB_VERSION "0.8.6" +# define PUSB_VERSION "0.8.7" #endif /* !PUSB_VERSION_H_ */
Vulnerability mechanics
Root cause
"Missing validation of the PINENTRY_FALLBACK_APP environment variable allows arbitrary command execution."
Attack vector
An attacker who can set environment variables before pamusb-pinentry is invoked sets PINENTRY_FALLBACK_APP to an arbitrary binary or script path. When pamusb-pinentry runs and the primary authentication method fails, it reads this environment variable and executes the pointed-to binary with the privileges of the pam_usb tool chain [patch_id=2779113]. The attacker must have local access and the ability to influence the environment of the pamusb-pinentry process (e.g., via a compromised user session or a setuid wrapper). No network path is required; the attack is purely local.
Affected code
The vulnerable code is in tools/pamusb-pinentry, where the PINENTRY_FALLBACK_APP environment variable was read and passed directly to subprocess.run() without validation. The patch modifies this file to add path validation checks before execution.
What the fix does
The patch in tools/pamusb-pinentry adds three checks before executing the fallback application: the path must be absolute (os.path.isabs), must point to an existing regular file (os.path.isfile), and must have the executable bit set (os.access with os.X_OK). Additionally, the subprocess call now passes the path as a list ([fallbackPinentryApp]) instead of a string, avoiding shell injection. If any check fails, the script prints an error to stderr and exits with code 1. These changes ensure that only a pre-existing, absolute executable path can be launched, closing the arbitrary-execution vector.
Preconditions
- inputAttacker must be able to set environment variables before pamusb-pinentry is invoked (e.g., via a compromised user session or setuid wrapper)
- networkAttacker must have local access to the system
- inputpamusb-pinentry must be invoked and the primary authentication method must fail (triggering the fallback path)
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.