VYPR
High severity7.9NVD Advisory· Published May 27, 2026

CVE-2026-44711

CVE-2026-44711

Description

pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.8.7, symlink attacks on pad directory and pad files enable authentication bypass and root file corruption. This vulnerability is fixed in 0.8.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Symlink attacks in pam_usb before 0.8.7 allow authentication bypass and arbitrary root file corruption via crafted pad directory and pad file symlinks.

Vulnerability

In pam_usb versions prior to 0.8.7, three related weaknesses in src/pad.c enable symlink attacks on the one-time pad directory and pad files. First, the check for ~/.pamusb/ uses stat() which follows symlinks, allowing a user to replace the directory with a symlink to an attacker-controlled location (H-4). Second, pad update files are opened with fopen() which follows symlinks, enabling a symlink to a root-owned file (H-2). Third, a partial read from a truncated pad file leaves uninitialized stack memory in the memcmp comparison (H-3). Affected versions: pam_usb <= 0.8.6 [1].

Exploitation

For H-4, an attacker removes ~/.pamusb/ and creates a symlink to a directory containing crafted pad files, then presents a USB device with matching pad content. For H-2, a user pre-places a symlink at ~/.pamusb/.pad.tmp pointing to a root-owned file (e.g., /etc/pam.d/sudo); on the next successful authentication, the PAM module (running as root) truncates and writes 1024 bytes of random data into that file. For H-3, an attacker provides a truncated pad file (e.g., 512 bytes) causing memcmp to compare 512 valid bytes plus 512 bytes of uninitialized stack memory, producing unpredictable authentication results [1].

Impact

Successful exploitation of H-4 allows authentication bypass without a registered USB device. H-2 enables corruption of arbitrary root-owned files, potentially leading to denial of service or privilege escalation. H-3 results in unpredictable authentication outcomes due to uninitialized memory comparison [1].

Mitigation

The vulnerability is fixed in pam_usb version 0.8.7. Users should upgrade to 0.8.7 or later. No workarounds are documented in the available references [1].

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

2
  • Mcdope/Pam Usbinferred2 versions
    <0.8.7+ 1 more
    • (no CPE)range: <0.8.7
    • (no CPE)range: <0.8.7

Patches

2
014042451850

[Security] Fix multiple advisories of various crititicallity - UPDATE ASAP!

https://github.com/mcdope/pam_usbMcDopeMay 4, 2026Fixed in 0.8.7via llm-release-walk
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(&regex, 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)
    
987f4f6ddf0a

Prepare v0.8.7

https://github.com/mcdope/pam_usbMcDopeMay 5, 2026Fixed in 0.8.7via release-tag
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 use of `lstat()` and `O_NOFOLLOW` in filesystem path handling allows symlink attacks, and incomplete `fread()` return-value checks allow uninitialised memory comparison."

Attack vector

An attacker with local unprivileged access can exploit three distinct weaknesses. For H-4 (authentication bypass), the attacker removes the user's `~/.pamusb/` directory and replaces it with a symlink to a directory they control; `stat()` follows the symlink, the directory is treated as existing, and pad files are read from the attacker's directory, allowing authentication with a crafted USB device [ref_id=1]. For H-2 (root file corruption), the attacker pre-places a symlink at `~/.pamusb/<Device>.pad.tmp` pointing to a root-owned file; on the next successful authentication, the PAM module (running as root) opens the symlink target with `fopen("w")`, truncating it and writing 1024 bytes of random data [ref_id=1]. For H-3, a truncated pad file causes `fread()` to return fewer than 1024 bytes, and `memcmp()` compares uninitialised stack memory against the system pad, yielding unpredictable authentication results [ref_id=1].

Affected code

The vulnerability resides in `src/pad.c` [ref_id=1]. Three code paths are at fault: the `stat()` call checking for `~/.pamusb/` (follows symlinks), `fopen()` calls for pad `.tmp` files (follows symlinks), and `fread()` calls in `pusb_pad_compare()` that do not verify the read returned the full 1024-byte buffer, leaving uninitialised stack memory in the subsequent `memcmp()` [ref_id=1].

What the fix does

The fix in commit `d9acb56` (`src/pad.c`) [ref_id=1] addresses all three weaknesses. The directory check was changed from `stat()` to `lstat()`; if `S_ISLNK` is set, the function immediately returns 0 (deny), preventing the symlink-based directory replacement. All pad file and `.tmp` file opens were changed from `fopen()` to `open(..., O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW)` + `fdopen()`, so pre-placed symlinks are rejected with `ELOOP`. Both `fread()` calls in `pusb_pad_compare()` now check that `bytes_read != sizeof(magic_*)` and return 0 (deny) on partial reads, eliminating the uninitialised memory comparison.

Preconditions

  • authAttacker must have local unprivileged access to the system (CVSS AV:L/PR:L)
  • inputFor H-4: attacker must be able to remove the target user's ~/.pamusb/ directory and create a symlink
  • inputFor H-2: attacker must be able to create a symlink at ~/.pamusb/.pad.tmp before the victim authenticates
  • inputFor H-3: attacker must be able to supply a truncated pad file (e.g. via the symlink attack in H-4)
  • authUser interaction required: the victim must authenticate via PAM while the symlink is in place (CVSS UI:R)

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.