repos: git inherits only an allow-listed environment

git_env copied the whole process environment, and git consults its
environment ahead of `-c`: GIT_ALLOW_PROTOCOL=file re-enabled file://
despite protocol.allow=never, and GIT_CONFIG_COUNT/KEY_*/VALUE_* could
add an `insteadOf` that rewrites an approved URL to another host.  Only
PATH, HOME, TMPDIR, TZ and the SSL certificate paths are inherited now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-10-06 17:32:59 -06:00
parent 1c214f4033
commit 1a28a5900a
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 40 additions and 2 deletions

View file

@ -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',

View file

@ -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