VYPR
Medium severity6.3NVD Advisory· Published May 27, 2026

CVE-2026-47274

CVE-2026-47274

Description

pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.9.0, multiple pam_usb helper tools resolved external binaries through the PATH environment variable rather than using absolute paths. An attacker who can influence the process environment during PAM authentication or tool execution could substitute malicious binaries. The affected tools are pamusb-check (src/tmux.c), pamusb-conf (tools/pamusb-conf), and pamusb-keyring-unlock-gnome (tools/pamusb-keyring-unlock-gnome). This vulnerability is fixed in 0.9.0.

AI Insight

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

pam_usb helper tools before 0.9.0 use non-absolute paths for external binaries, allowing PATH hijacking and potential privilege escalation.

Vulnerability

pam_usb prior to version 0.9.0 resolves external binaries via the PATH environment variable rather than using absolute paths. The affected tools are pamusb-check (in src/tmux.c), pamusb-conf (in tools/pamusb-conf), and pamusb-keyring-unlock-gnome (in tools/pamusb-keyring-unlock-gnome) [3]. For example, pamusb-check invoked tmux without an absolute path [4], pamusb-conf invoked findmnt via PATH [2], and pamusb-keyring-unlock-gnome resolved over ten binaries including id, logger, and gnome-keyring-daemon through PATH [1].

Exploitation

An attacker who can influence the process environment (e.g., by controlling the PATH variable during PAM authentication or tool execution) can substitute malicious binaries. For pamusb-conf, typically run with elevated privileges, this allows privilege escalation by redirecting findmnt to an arbitrary executable [3]. For pamusb-keyring-unlock-gnome, in addition to PATH hijacking, the unlock password was previously sourced via shell eval from a file, enabling injection if the file content is attacker-controlled [1].

Impact

An attacker who successfully exploits this vulnerability can execute arbitrary code with the privileges of the pam_usb process. Since pamusb-conf often runs as root, this can lead to full system compromise. Other tools may lead to credential theft or local privilege escalation depending on the context [3].

Mitigation

The vulnerability is fixed in pam_usb version 0.9.0. The fixes are implemented in commits [1], [2], and [4], which replace all external binary invocations with absolute paths and address the password injection vector in pamusb-keyring-unlock-gnome. Users should upgrade to version 0.9.0 or later. No workarounds are available for earlier versions.

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.

Patches

3
1ee874592038

Harden keyring auth check path (#323)

https://github.com/mcdope/pam_usbJeremyTsangMay 17, 2026via nvd-ref
3 files changed · +112 21
  • tests/can-actually-be-used/test-keyring-unlock-gnome-installer.sh+1 1 modified
    @@ -14,7 +14,7 @@ printf 'testpassword\n' | HOME=$FAKE_HOME $TOOL --install
     [ -f "$FAKE_HOME/.pamusb/.keyring_unlock_password" ]
     PERMS=$(stat -c "%a" "$FAKE_HOME/.pamusb/.keyring_unlock_password")
     [ "$PERMS" = "600" ]
    -grep -q 'UNLOCK_PASSWORD="testpassword"' "$FAKE_HOME/.pamusb/.keyring_unlock_password"
    +grep -qx 'UNLOCK_PASSWORD=testpassword' "$FAKE_HOME/.pamusb/.keyring_unlock_password"
     
     # verify autostart desktop file created with autostart enabled
     [ -f "$FAKE_HOME/.config/autostart/pamusb-keyring-unlock-gnome.desktop" ]
    
  • tests/unit/python/test_pamusb_keyring_unlock_gnome.py+73 0 added
    @@ -0,0 +1,73 @@
    +"""
    +Unit tests for tools/pamusb-keyring-unlock-gnome.
    +
    +Security regression coverage:
    +  K-1: authentication check invokes /usr/bin/pamusb-check, not PATH lookup
    +  K-1: username lookup invokes /usr/bin/id, not PATH lookup
    +  K-2: stored password is read as data without sourcing shell code
    +"""
    +
    +from pathlib import Path
    +import subprocess
    +
    +
    +SCRIPT = Path(__file__).resolve().parents[3] / "tools" / "pamusb-keyring-unlock-gnome"
    +
    +
    +def test_script_syntax_is_valid():
    +    subprocess.run(["sh", "-n", str(SCRIPT)], check=True)
    +
    +
    +def test_auth_check_uses_absolute_pamusb_check():
    +    text = SCRIPT.read_text()
    +
    +    assert "PAMUSB_CHECK=/usr/bin/pamusb-check" in text
    +    assert '"$PAMUSB_CHECK" "$USER_NAME"' in text
    +    assert "pamusb-check `whoami`" not in text
    +    assert "pamusb-check $(whoami)" not in text
    +
    +
    +def test_username_lookup_uses_absolute_id():
    +    text = SCRIPT.read_text()
    +
    +    assert "ID=/usr/bin/id" in text
    +    assert 'USER_NAME=$("$ID" -un)' in text
    +    assert "`whoami`" not in text
    +    assert "$(whoami)" not in text
    +
    +
    +def test_password_file_is_not_sourced_as_shell():
    +    text = SCRIPT.read_text()
    +
    +    assert "KEYFILE=" in text
    +    assert "source " not in text
    +    assert ". \"$KEYFILE\"" not in text
    +    assert "UNLOCK_PASSWORD=$(grep" not in text
    +    assert 'UNLOCK_PASSWORD=$("$SED" -n' in text
    +    assert 'printf \'UNLOCK_PASSWORD=%s\\n\'' in text
    +
    +
    +def test_quoted_password_compat_requires_closing_quote():
    +    text = SCRIPT.read_text()
    +
    +    assert r"""    '"'*'"') UNLOCK_PASSWORD=${UNLOCK_PASSWORD#\"}; UNLOCK_PASSWORD=${UNLOCK_PASSWORD%\"} ;;""" in text
    +    assert r'    \"*\") UNLOCK_PASSWORD=${UNLOCK_PASSWORD#\"}; UNLOCK_PASSWORD=${UNLOCK_PASSWORD%\"} ;;' not in text
    +
    +
    +def test_auth_check_exits_script_not_subshell():
    +    text = SCRIPT.read_text()
    +
    +    assert "|| (" not in text
    +    assert "||(" not in text.replace(" ", "")
    +    assert 'if ! "$PAMUSB_CHECK" "$USER_NAME"' in text
    +    assert "exit 1" in text
    +
    +
    +def test_keyring_commands_use_absolute_paths():
    +    text = SCRIPT.read_text()
    +
    +    assert "PIDOF=/usr/bin/pidof" in text
    +    assert "KILL=/usr/bin/kill" in text
    +    assert "GNOME_KEYRING_DAEMON=/usr/bin/gnome-keyring-daemon" in text
    +    assert "`pidof gnome-keyring-daemon`" not in text
    +    assert '"$GNOME_KEYRING_DAEMON" --daemonize --login' in text
    
  • tools/pamusb-keyring-unlock-gnome+38 20 modified
    @@ -17,6 +17,17 @@
     
     INSTALL=0
     UNINSTALL=0
    +PAMUSB_CHECK=/usr/bin/pamusb-check
    +ID=/usr/bin/id
    +LOGGER=/usr/bin/logger
    +STAT=/usr/bin/stat
    +AWK=/usr/bin/awk
    +SED=/usr/bin/sed
    +HEAD=/usr/bin/head
    +PIDOF=/usr/bin/pidof
    +KILL=/usr/bin/kill
    +GNOME_KEYRING_DAEMON=/usr/bin/gnome-keyring-daemon
    +KEYFILE="$HOME/.pamusb/.keyring_unlock_password"
     
     while [ "$1" != "" ]; do
         case $1 in
    @@ -28,7 +39,7 @@ while [ "$1" != "" ]; do
     done
     
     do_install() {
    -    if [ "$(id -u)" = "0" ]; then
    +    if [ "$("$ID" -u)" = "0" ]; then
             printf 'Do not run --install as root; files belong in your user home.\n' >&2
             exit 1
         fi
    @@ -44,8 +55,8 @@ do_install() {
         fi
     
         mkdir -p "$HOME/.pamusb"
    -    printf 'UNLOCK_PASSWORD="%s"\n' "$UNLOCK_PASSWORD" > "$HOME/.pamusb/.keyring_unlock_password"
    -    chmod 0600 "$HOME/.pamusb/.keyring_unlock_password"
    +    printf 'UNLOCK_PASSWORD=%s\n' "$UNLOCK_PASSWORD" > "$KEYFILE"
    +    chmod 0600 "$KEYFILE"
     
         mkdir -p "$HOME/.config/autostart"
     
    @@ -71,7 +82,7 @@ EOF
     }
     
     do_uninstall() {
    -    rm -f "$HOME/.pamusb/.keyring_unlock_password"
    +    rm -f "$KEYFILE"
         rm -f "$HOME/.config/autostart/pamusb-keyring-unlock-gnome.desktop"
         rm -f "$HOME/.config/autostart/gnome-keyring-secrets.desktop"
         printf 'Uninstall complete. Removed password file and autostart entries.\n'
    @@ -88,40 +99,47 @@ if [ "$UNINSTALL" = "1" ]; then
     fi
     
     # Check for valid authentication
    -pamusb-check `whoami` > /dev/null 2>&1 || (logger -p local0.error -t ${0##*/}[$$] pamusb-check failed. && exit 1)
    +USER_NAME=$("$ID" -un)
    +if ! "$PAMUSB_CHECK" "$USER_NAME" > /dev/null 2>&1; then
    +    "$LOGGER" -p local0.error -t "${0##*/}[$$]" pamusb-check failed.
    +    exit 1
    +fi
     
     # Check if password file exists
    -if [ ! -f ~/.pamusb/.keyring_unlock_password ]; then
    -    logger -p local0.notice -t ${0##*/}[$$] No password file found, exiting.
    +if [ ! -f "$KEYFILE" ]; then
    +    "$LOGGER" -p local0.notice -t "${0##*/}[$$]" No password file found, exiting.
         exit 0
     fi
     
     # Ensure file has 0600 (if FS supports it)
    -PERMISSIONS=`stat -c "%a %n" ~/.pamusb/.keyring_unlock_password | awk '{print $1}'`
    +PERMISSIONS=$("$STAT" -c "%a %n" "$KEYFILE" | "$AWK" '{print $1}')
     if [ ! "$PERMISSIONS" = "600" ]; then
    -    logger -p local0.error -t ${0##*/}[$$] Bad permissions on ~/.pamusb/.keyring_unlock_password. Please change them to 0600.
    +    "$LOGGER" -p local0.error -t "${0##*/}[$$]" Bad permissions on ~/.pamusb/.keyring_unlock_password. Please change them to 0600.
         exit 1
     fi
     
     # Kill existing keyring instance
    -kill `pidof gnome-keyring-daemon`
    -if [ ! "$?" = "0" ]; then
    -    logger -p local0.notice -t ${0##*/}[$$] Couldn\'t kill existing gnome-keyring-instance - was it launched already?
    +KEYRING_PIDS=$("$PIDOF" gnome-keyring-daemon)
    +if [ -n "$KEYRING_PIDS" ] && "$KILL" $KEYRING_PIDS; then
    +    "$LOGGER" -p local0.notice -t "${0##*/}[$$]" Killed existing gnome-keyring-instance.
     else
    -    logger -p local0.notice -t ${0##*/}[$$] Killed existing gnome-keyring-instance.
    +    "$LOGGER" -p local0.notice -t "${0##*/}[$$]" Couldn\'t kill existing gnome-keyring-instance - was it launched already?
     fi
     
    -# Read UNLOCK_PASSWORD from $KEYFILE via grep to avoid sourcing arbitrary shell
    -UNLOCK_PASSWORD=$(grep -oP '^UNLOCK_PASSWORD=\K.*' ~/.pamusb/.keyring_unlock_password)
    +# Read UNLOCK_PASSWORD without sourcing the file as shell code.
    +UNLOCK_PASSWORD=$("$SED" -n 's/^UNLOCK_PASSWORD=//p' "$KEYFILE" | "$HEAD" -n 1)
    +case "$UNLOCK_PASSWORD" in
    +    '"'*'"') UNLOCK_PASSWORD=${UNLOCK_PASSWORD#\"}; UNLOCK_PASSWORD=${UNLOCK_PASSWORD%\"} ;;
    +esac
     if [ -z "$UNLOCK_PASSWORD" ]; then
    -    logger -p local0.error -t ${0##*/}[$$] Could not read UNLOCK_PASSWORD from file.
    +    "$LOGGER" -p local0.error -t "${0##*/}[$$]" Could not read UNLOCK_PASSWORD from file.
         exit 1
     fi
     
     # 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.
    +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.
     
     exit $UNLOCKED
    
52a1fd6413b7

Harden reset-pads path handling

https://github.com/mcdope/pam_usbOrange StudioMay 15, 2026via nvd-ref
2 files changed · +34 6
  • tests/unit/python/test_pamusb_conf.py+23 4 modified
    @@ -10,6 +10,7 @@
       - C-2 regression: UUID with shell metacharacters is rejected
       - C-2 regression: UUID with path traversal is rejected
       - C-2 regression: valid UUID is accepted
    +  - resetPads uses passwd home directories and an absolute findmnt path
     """
     
     import sys
    @@ -222,10 +223,12 @@ def test_reset_pads_processes_all_devices_when_connected(tmp_path):
     
         with patch.object(_mod, "minidom") as mock_mini, \
              patch.object(_mod, "os") as mock_os, \
    +         patch.object(_mod, "pwd") as mock_pwd, \
              patch.object(_mod, "subprocess") as mock_sub, \
              patch.object(_mod, "socket") as mock_sock:
     
             mock_mini.parse.return_value = minidom.parseString(_TWO_DEVICE_XML)
    +        mock_pwd.getpwnam.return_value.pw_dir = "/srv/home/testuser"
             mock_os.remove.side_effect = lambda p: removed.append(p)
             mock_sub.check_output.side_effect = [b"/mnt/dev1\n", b"/mnt/dev2\n"]
             mock_sub.CalledProcessError = subprocess.CalledProcessError
    @@ -237,11 +240,12 @@ def test_reset_pads_processes_all_devices_when_connected(tmp_path):
             with pytest.raises(SystemExit):
                 _mod.resetPads()
     
    -    assert "/home/testuser/.pamusb/dev1.pad" in removed
    -    assert "/home/testuser/.pamusb/dev2.pad" in removed
    +    assert "/srv/home/testuser/.pamusb/dev1.pad" in removed
    +    assert "/srv/home/testuser/.pamusb/dev2.pad" in removed
         assert "/mnt/dev1/.pamusb/testuser.testhost.pad" in removed
         assert "/mnt/dev2/.pamusb/testuser.testhost.pad" in removed
         assert len(removed) == 4
    +    assert mock_sub.check_output.call_args_list[0].args[0][0] == "/usr/bin/findmnt"
     
     
     def test_reset_pads_skips_disconnected_device_atomically(tmp_path):
    @@ -250,10 +254,12 @@ def test_reset_pads_skips_disconnected_device_atomically(tmp_path):
     
         with patch.object(_mod, "minidom") as mock_mini, \
              patch.object(_mod, "os") as mock_os, \
    +         patch.object(_mod, "pwd") as mock_pwd, \
              patch.object(_mod, "subprocess") as mock_sub, \
              patch.object(_mod, "socket") as mock_sock:
     
             mock_mini.parse.return_value = minidom.parseString(_TWO_DEVICE_XML)
    +        mock_pwd.getpwnam.return_value.pw_dir = "/srv/home/testuser"
             mock_os.remove.side_effect = lambda p: removed.append(p)
             mock_sub.check_output.side_effect = [
                 b"/mnt/dev1\n",
    @@ -268,10 +274,23 @@ def test_reset_pads_skips_disconnected_device_atomically(tmp_path):
             with pytest.raises(SystemExit):
                 _mod.resetPads()
     
    -    assert "/home/testuser/.pamusb/dev1.pad" in removed
    +    assert "/srv/home/testuser/.pamusb/dev1.pad" in removed
         assert "/mnt/dev1/.pamusb/testuser.testhost.pad" in removed
         assert len(removed) == 2
    -    assert "/home/testuser/.pamusb/dev2.pad" not in removed
    +    assert "/srv/home/testuser/.pamusb/dev2.pad" not in removed
    +
    +
    +def test_reset_pads_rejects_unknown_user():
    +    """resetPads should fail before building deletion paths for unknown users."""
    +    with patch.object(_mod, "minidom") as mock_mini, \
    +         patch.object(_mod, "pwd") as mock_pwd:
    +
    +        mock_mini.parse.return_value = minidom.parseString(_TWO_DEVICE_XML)
    +        mock_pwd.getpwnam.side_effect = KeyError
    +        _mod.options = {"configFile": "/fake/conf", "resetPads": "missinguser"}
    +
    +        with pytest.raises(SystemExit):
    +            _mod.resetPads()
     
     
     def _uuid_is_valid(uuid: str) -> bool:
    
  • tools/pamusb-conf+11 2 modified
    @@ -22,12 +22,15 @@ import gi
     import subprocess
     import socket
     import getopt
    +import pwd
     
     gi.require_version('UDisks', '2.0')
     
     from gi.repository import UDisks
     from xml.dom import minidom
     
    +FINDMNT_PATH = '/usr/bin/findmnt'
    +
     class Device:
     	def __init__(self, udi):
     		self.__udi = udi
    @@ -333,6 +336,7 @@ def addDevice(options):
     def resetPads():
     	padFiles = []
     	skippedDevices = []
    +	userHome = None
     
     	try:
     		doc = minidom.parse(options['configFile'])
    @@ -347,6 +351,11 @@ def resetPads():
     	if len(devicesObj) == 0:
     		print('No devices found.')
     		sys.exit(1)
    +	try:
    +		userHome = pwd.getpwnam(options['resetPads']).pw_dir
    +	except KeyError:
    +		print('Unable to retrieve information for user "%s".' % options['resetPads'])
    +		sys.exit(1)
     
     	alreadyConfiguredUsers = doc.getElementsByTagName('user')
     	if len(alreadyConfiguredUsers) > 0:
    @@ -358,7 +367,7 @@ def resetPads():
     						print('Invalid device ID in config (path traversal): %s' % userDevice)
     						sys.exit(1)
     
    -					systemPad = '/home/%s/.pamusb/%s.pad' % (options['resetPads'], userDevice)
    +					systemPad = '%s/.pamusb/%s.pad' % (userHome.rstrip('/'), userDevice)
     					devicePad = None
     
     					for details in devicesObj:
    @@ -369,7 +378,7 @@ def resetPads():
     								sys.exit(1)
     							try:
     								deviceMountPoint = subprocess.check_output(
    -									['findmnt', '-n', '-o', 'TARGET', '--source', '/dev/disk/by-uuid/' + deviceUUID],
    +									[FINDMNT_PATH, '-n', '-o', 'TARGET', '--source', '/dev/disk/by-uuid/' + deviceUUID],
     									stderr=subprocess.DEVNULL
     								)
     								devicePad = '%s/.pamusb/%s.%s.pad' % (
    
993e73d8bebb

Harden tmux command lookup

https://github.com/mcdope/pam_usbnicMay 14, 2026via nvd-ref
2 files changed · +27 4
  • src/tmux.c+7 3 modified
    @@ -102,9 +102,13 @@ char *pusb_tmux_get_client_tty(pid_t env_pid)
             return NULL;
         }
     
    -    size_t get_tmux_session_details_cmd_len = strlen(tmux_socket_path) + strlen(tmux_client_id) + 64;
    +    size_t get_tmux_session_details_cmd_len =
    +        strlen("LC_ALL=C; /usr/bin/tmux -S \"\" list-clients -t \"\\$\"")
    +        + strlen(tmux_socket_path)
    +        + strlen(tmux_client_id)
    +        + 1;
         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);
    +    snprintf(get_tmux_session_details_cmd, get_tmux_session_details_cmd_len, "LC_ALL=C; /usr/bin/tmux -S \"%s\" list-clients -t \"\\$%s\"", tmux_socket_path, tmux_client_id);
         log_debug("		Built get_tmux_session_details_cmd: %s\n", get_tmux_session_details_cmd);
     
         char buf[BUFSIZ];
    @@ -221,4 +225,4 @@ int pusb_tmux_has_remote_clients(const char* username)
     
         // If we had detected a remote access we would have returned by now. Safe to return 0 now
         return 0;
    -}
    \ No newline at end of file
    +}
    
  • tests/unit/c/tmux_test.c+20 1 modified
    @@ -8,6 +8,7 @@
      *   C-1: non-numeric TMUX client ID rejected
      *   C-1: pusb_tmux_escape_for_regex handles all 14 ERE metacharacters
      *   C-1: dot in username escaped so it doesn't match a different user
    + *   C-2: tmux command is resolved by absolute path, not PAM-time PATH
      */
     
     #include <stdarg.h>
    @@ -25,10 +26,11 @@
     /* ── popen/pclose mock ── */
     
     static const char *g_popen_output = "";
    +static char g_last_popen_cmd[BUFSIZ];
     
     FILE *__wrap_popen(const char *cmd, const char *type)
     {
    -	(void)cmd;
    +	snprintf(g_last_popen_cmd, sizeof(g_last_popen_cmd), "%s", cmd);
     	return fmemopen((void *)g_popen_output, strlen(g_popen_output), type);
     }
     
    @@ -225,6 +227,22 @@ static void test_get_client_tty_valid(void **state)
     	unsetenv("TMUX");
     }
     
    +static void test_get_client_tty_uses_absolute_tmux_path(void **state)
    +{
    +	(void)state;
    +	setenv("TMUX", "/tmp/tmux-1000/default,abc,42", 1);
    +	g_popen_output = "/dev/pts/1: session info\n";
    +	memset(g_last_popen_cmd, 0, sizeof(g_last_popen_cmd));
    +
    +	char *result = pusb_tmux_get_client_tty(0);
    +
    +	assert_non_null(result);
    +	assert_non_null(strstr(g_last_popen_cmd, "LC_ALL=C; /usr/bin/tmux -S "));
    +	assert_null(strstr(g_last_popen_cmd, "LC_ALL=C; tmux -S "));
    +	free(result);
    +	unsetenv("TMUX");
    +}
    +
     /* ── main ── */
     
     int main(void)
    @@ -258,6 +276,7 @@ int main(void)
     		cmocka_unit_test(test_get_client_tty_injection_semicolon),
     		cmocka_unit_test(test_get_client_tty_nonnumeric_id),
     		cmocka_unit_test(test_get_client_tty_valid),
    +		cmocka_unit_test(test_get_client_tty_uses_absolute_tmux_path),
     	};
     	return cmocka_run_group_tests(tests, NULL, NULL);
     }
    

Vulnerability mechanics

Root cause

"Multiple pam_usb helper tools resolved external binaries through the PATH environment variable rather than using absolute paths, allowing an attacker who controls PATH to substitute malicious executables."

Attack vector

An attacker who can influence the process environment (e.g., by controlling the PATH variable during PAM authentication or tool execution) can substitute malicious binaries for any of the externally resolved commands. For pamusb-check (src/tmux.c), the `tmux` binary is invoked during local session verification; an attacker controlling PATH could redirect this to an arbitrary executable [ref_id=3]. For pamusb-conf, `findmnt` is called during --reset-pads processing, which typically runs with elevated privileges, making PATH hijacking particularly impactful [ref_id=3]. For pamusb-keyring-unlock-gnome, more than ten binaries were resolved via PATH, and the unlock password file was sourced as shell code, allowing injection if the file content is attacker-controlled [ref_id=3]. The CVSS vector indicates local access, high attack complexity, and low privileges required.

Affected code

Three helper tools are affected. In src/tmux.c, the function pusb_tmux_get_client_tty invoked `tmux` via a relative PATH lookup [patch_id=2749077]. In tools/pamusb-conf, the resetPads() function called `findmnt` without an absolute path [patch_id=2749076]. In tools/pamusb-keyring-unlock-gnome, over ten external binaries (pamusb-check, id, logger, stat, awk, sed, head, pidof, kill, gnome-keyring-daemon) were resolved through PATH, and the password file was sourced as shell code rather than read as data [patch_id=2749075].

What the fix does

All three patches harden external binary resolution by replacing relative PATH lookups with absolute paths. In src/tmux.c, `tmux` is changed to `/usr/bin/tmux` [patch_id=2749077]. In tools/pamusb-conf, `findmnt` is replaced with the constant `FINDMNT_PATH = '/usr/bin/findmnt'`, and the user's home directory is now obtained via `pwd.getpwnam()` instead of hardcoding `/home/` [patch_id=2749076]. In tools/pamusb-keyring-unlock-gnome, every external binary is assigned an absolute-path variable (e.g., `PAMUSB_CHECK=/usr/bin/pamusb-check`), the password file is read as data via `sed`/`head` instead of being sourced as shell code, and the password is passed to gnome-keyring-daemon via stdin rather than as a command-line argument [patch_id=2749075].

Preconditions

  • configAttacker must be able to influence the PATH environment variable during PAM authentication or tool execution (local access required).
  • authFor pamusb-conf, the tool must be run with elevated privileges (e.g., sudo) for the --reset-pads operation.
  • inputFor pamusb-keyring-unlock-gnome, the attacker must be able to write to or control the content of the keyring password file to exploit the shell-sourcing issue.

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

References

4

News mentions

0

No linked articles in our index yet.