mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-19 01:15:04 -05:00
Fix MASTER_PASSWORD_HOOK tokenisation on Windows
posix=False in shlex.split() never strips quote characters, so any Windows hook path quoted to handle spaces (or any quoted argument) came out with literal quotes still attached, and unquoted spaced paths were split into multiple argv elements either way. Use a shlex.shlex instance with posix=True (correct quote-stripping) and escape='' (so backslashes in Windows paths are not treated as escape characters). Document the quoting requirement in config.py and add regression tests for quoted/unquoted spaced paths.
This commit is contained in:
+3
-1
@@ -641,7 +641,9 @@ USE_OS_SECRET_STORAGE = True
|
||||
# shell, so shell features (pipes, redirection, environment-variable
|
||||
# expansion, globbing) in this value are not interpreted; put any such
|
||||
# logic inside the hook script itself. On Windows, a batch file must be
|
||||
# invoked via an executable wrapper rather than directly.
|
||||
# invoked via an executable wrapper rather than directly. If the path to
|
||||
# the script/program contains spaces, quote it, e.g. -
|
||||
# MASTER_PASSWORD_HOOK = '"<PATH WITH SPACES>/passwdgen_script.exe" %u'
|
||||
##########################################################################
|
||||
MASTER_PASSWORD_HOOK = None
|
||||
|
||||
|
||||
@@ -192,7 +192,6 @@ def get_master_password_from_master_hook():
|
||||
|
||||
:return: Output of the command, or None on failure.
|
||||
"""
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
@@ -204,10 +203,16 @@ def get_master_password_from_master_hook():
|
||||
# untrusted username into the individual arguments. Substituting
|
||||
# before tokenising would let metacharacters in the username alter
|
||||
# the command, so the order here is security-critical.
|
||||
args = [
|
||||
token.replace('%u', current_user.username)
|
||||
for token in shlex.split(cmd, posix=(os.name != 'nt'))
|
||||
]
|
||||
#
|
||||
# posix=True strips quotes correctly on both platforms (unlike
|
||||
# posix=False, which never strips quotes at all and breaks any
|
||||
# quoted path or argument on Windows). escape='' disables backslash
|
||||
# escaping so Windows paths (e.g. C:\Program Files\hook.exe) are not
|
||||
# mangled.
|
||||
lex = shlex.shlex(cmd, posix=True)
|
||||
lex.whitespace_split = True
|
||||
lex.escape = ''
|
||||
args = [token.replace('%u', current_user.username) for token in lex]
|
||||
if not args:
|
||||
return None
|
||||
|
||||
|
||||
@@ -87,6 +87,43 @@ class TestMasterPasswordHookConfinesUsername(BaseTestGenerator):
|
||||
self.assertEqual(output, 'secret-key')
|
||||
|
||||
|
||||
class TestMasterPasswordHookQuotedPath(BaseTestGenerator):
|
||||
"""A quoted hook program path (needed when the path contains spaces,
|
||||
e.g. 'C:\\Program Files\\hook.exe' on Windows) must be tokenised as a
|
||||
single argv element with the quotes stripped, and any backslashes in
|
||||
the path must survive untouched."""
|
||||
|
||||
scenarios = [
|
||||
('quoted path with spaces, posix-style',
|
||||
dict(cmd='"/opt/my secrets/get-secret" %u',
|
||||
expected_argv0='/opt/my secrets/get-secret')),
|
||||
('quoted windows path with backslashes and spaces',
|
||||
dict(cmd='"C:\\Program Files\\hook.exe" %u',
|
||||
expected_argv0='C:\\Program Files\\hook.exe')),
|
||||
('unquoted windows path, no spaces',
|
||||
dict(cmd='C:\\hook.exe %u', expected_argv0='C:\\hook.exe')),
|
||||
]
|
||||
|
||||
def runTest(self):
|
||||
captured = {}
|
||||
|
||||
def fake_popen(args, *a, **kw):
|
||||
captured['args'] = args
|
||||
return _FakeProc()
|
||||
|
||||
fake_user = MagicMock()
|
||||
fake_user.username = 'alice'
|
||||
|
||||
with patch.object(mp, 'current_user', fake_user), \
|
||||
patch.object(mp.config, 'MASTER_PASSWORD_HOOK', self.cmd), \
|
||||
patch('subprocess.Popen', side_effect=fake_popen):
|
||||
mp.get_master_password_from_master_hook()
|
||||
|
||||
self.assertEqual(captured['args'][0], self.expected_argv0)
|
||||
self.assertEqual(captured['args'][1], 'alice')
|
||||
self.assertEqual(len(captured['args']), 2)
|
||||
|
||||
|
||||
class TestMasterPasswordHookNoShellExecution(BaseTestGenerator):
|
||||
"""End-to-end PoC regression (POSIX only): a username containing shell
|
||||
syntax must not cause command execution. Mirrors the reported proof of
|
||||
|
||||
Reference in New Issue
Block a user