diff --git a/modules/repos/lib.py b/modules/repos/lib.py index 39974ef3..b2470cec 100644 --- a/modules/repos/lib.py +++ b/modules/repos/lib.py @@ -219,9 +219,16 @@ def git_command(*args, config: tuple = ()) -> tuple: return GIT_BIN, *safety, *args +# The only parts of WROLPi's environment git may see. Anything else (GIT_ALLOW_PROTOCOL, GIT_CONFIG_COUNT and +# GIT_CONFIG_KEY_*/VALUE_*, GIT_CONFIG_PARAMETERS, GIT_SSL_NO_VERIFY, proxies, ...) could undo `git_command`'s +# restrictions. +GIT_ENV_ALLOWED = ('PATH', 'HOME', 'TMPDIR', 'TZ', 'SSL_CERT_FILE', 'SSL_CERT_DIR') + + def git_env() -> dict: - """Environment for git: never prompt for credentials, ignore the user's and system's git configs.""" - env = dict(os.environ) + """Environment for git: never prompt for credentials, ignore the user's and system's git configs, and inherit + nothing that changes how git connects.""" + env = {k: v for k, v in os.environ.items() if k in GIT_ENV_ALLOWED} env.update( GIT_TERMINAL_PROMPT='0', GIT_ASKPASS='true', diff --git a/modules/repos/test/test_lib.py b/modules/repos/test/test_lib.py index 8182bf1f..b74053d0 100644 --- a/modules/repos/test/test_lib.py +++ b/modules/repos/test/test_lib.py @@ -1,4 +1,7 @@ +import os + import pytest +from mock import mock from modules.repos import lib from modules.repos.errors import InvalidRepo, RepoConflict @@ -276,3 +279,31 @@ async def test_repo_directory_traversal(test_session, test_directory, async_clie lib.ReposConfig._import_repo(test_session, dict(url='https://github.com/a/b', directory='../outside')) test_session.rollback() assert test_session.query(Repository).one().directory == test_directory / 'repos/b' + + +def test_git_env_is_not_inherited(): + """The process environment cannot undo git_command's restrictions.""" + hostile = dict( + GIT_ALLOW_PROTOCOL='file', GIT_CONFIG_COUNT='1', GIT_CONFIG_KEY_0='url.https://127.0.0.1/.insteadOf', + GIT_CONFIG_VALUE_0='https://example.com/', GIT_CONFIG_PARAMETERS="'protocol.allow'='always'", + GIT_SSL_NO_VERIFY='1', GIT_PROXY_COMMAND='evil', GIT_SSH_COMMAND='evil', HTTPS_PROXY='http://127.0.0.1:9', + https_proxy='http://127.0.0.1:9', CURL_CA_BUNDLE='/tmp/evil.pem', GIT_DIR='/tmp/evil', + ) + with mock.patch.dict(os.environ, dict(hostile, PATH='/usr/bin:/bin', SSL_CERT_FILE='/etc/ssl/cert.pem')): + env = lib.git_env() + assert not set(hostile) & set(env), set(hostile) & set(env) + assert env['PATH'] == '/usr/bin:/bin' + assert env['SSL_CERT_FILE'] == '/etc/ssl/cert.pem' + assert env['GIT_TERMINAL_PROMPT'] == '0' + + +def test_git_command_ignores_hostile_environment(tmp_path): + """With git's environment variables set against it, git still refuses file:// (only https is allowed).""" + import subprocess + origin = tmp_path / 'origin' + subprocess.run(['git', 'init', '--quiet', str(origin)], check=True) + with mock.patch.dict(os.environ, dict(GIT_ALLOW_PROTOCOL='file')): + result = subprocess.run(lib.git_command('ls-remote', f'file://{origin}'), env=lib.git_env(), + capture_output=True) + assert result.returncode != 0 + assert b"transport 'file' not allowed" in result.stderr