mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
A repo's .gitmodules is chosen by whoever controls the repo, so a submodule URL could make WROLPi fetch https://localhost:8443/..., https://10.0.0.1/..., etc. (SSRF), and show the response's error. Every submodule URL is now checked before git fetches it: relative URLs, the repo's own host, and hosts whose addresses are all public are allowed; anything else defers the download with the reason. Nested submodules are checked out one level at a time (instead of git's --recursive) so each level is checked, at most 5 levels deep. Submodule fetches do not follow HTTP redirects, so a checked host cannot redirect git into the network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.9 KiB
Python
97 lines
3.9 KiB
Python
import os
|
|
import pathlib
|
|
import subprocess
|
|
from typing import Callable
|
|
|
|
import pytest
|
|
from mock import mock
|
|
|
|
from modules.repos import lib
|
|
from modules.repos.downloader import GitDownloader
|
|
from wrolpi.downloader import DownloadManager
|
|
|
|
# Commits made by the tests have a fixed identity and do not read the developer's git config.
|
|
TEST_GIT_ENV = dict(
|
|
GIT_AUTHOR_NAME='WROLPi Test', GIT_AUTHOR_EMAIL='test@example.com',
|
|
GIT_COMMITTER_NAME='WROLPi Test', GIT_COMMITTER_EMAIL='test@example.com',
|
|
GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
|
|
)
|
|
|
|
|
|
class GitRemote:
|
|
"""A local repo which the Repos module sees as `https://git.example.com/<owner>/<name>`."""
|
|
|
|
def __init__(self, path: pathlib.Path, url: str):
|
|
self.path = path
|
|
self.url = url
|
|
|
|
def git(self, *args: str) -> str:
|
|
env = dict(os.environ, **TEST_GIT_ENV)
|
|
result = subprocess.run(['git', *args], cwd=self.path, env=env, check=True, capture_output=True)
|
|
return result.stdout.decode().strip()
|
|
|
|
def commit(self, files: dict, message: str = 'Commit') -> str:
|
|
"""Write `files` ({relative path: contents}), commit them, return the commit's sha."""
|
|
for name, contents in files.items():
|
|
file = self.path / name
|
|
file.parent.mkdir(parents=True, exist_ok=True)
|
|
file.write_text(contents)
|
|
self.git('add', '--all')
|
|
self.git('commit', '--quiet', '-m', message)
|
|
return self.git('rev-parse', 'HEAD')
|
|
|
|
@property
|
|
def head(self) -> str:
|
|
return self.git('rev-parse', 'HEAD')
|
|
|
|
def add_submodule(self, path: str, submodule: 'GitRemote', url: str = None) -> str:
|
|
"""Add `submodule` (at its current commit) at `path`, and commit it. `url` overrides the submodule's URL."""
|
|
gitmodules = self.path / '.gitmodules'
|
|
existing = gitmodules.read_text() if gitmodules.exists() else ''
|
|
gitmodules.write_text(f'{existing}[submodule "{path}"]\n\tpath = {path}\n\turl = {url or submodule.url}\n')
|
|
self.git('update-index', '--add', '--cacheinfo', f'160000,{submodule.head},{path}')
|
|
self.git('add', '.gitmodules')
|
|
self.git('commit', '--quiet', '-m', f'Add submodule {path}')
|
|
return self.head
|
|
|
|
|
|
@pytest.fixture
|
|
def git_remote_factory(tmp_path_factory) -> Callable[..., GitRemote]:
|
|
"""Create local git repos which are cloned over `https://git.example.com/...` URLs.
|
|
|
|
git's `url.<base>.insteadOf` rewrites the https URL to the local path, and `file://` is allowed for the test,
|
|
so the Repos module runs the same commands it runs in production."""
|
|
remotes = dict()
|
|
# Outside the media directory (which is `tmp_path`).
|
|
remotes_directory = tmp_path_factory.mktemp('remotes')
|
|
|
|
def git_env():
|
|
env = original_git_env()
|
|
env['GIT_CONFIG_COUNT'] = str(len(remotes))
|
|
for idx, (url, path) in enumerate(remotes.items()):
|
|
env[f'GIT_CONFIG_KEY_{idx}'] = f'url.file://{path}.insteadOf'
|
|
env[f'GIT_CONFIG_VALUE_{idx}'] = url
|
|
return env
|
|
|
|
original_git_env = lib.git_env
|
|
|
|
def factory(name: str = 'example', owner: str = 'owner', branch: str = 'main',
|
|
files: dict = None) -> GitRemote:
|
|
path = remotes_directory / owner / name
|
|
path.mkdir(parents=True)
|
|
url = f'https://git.example.com/{owner}/{name}'
|
|
remote = GitRemote(path, url)
|
|
remote.git('init', '--quiet', f'--initial-branch={branch}')
|
|
remote.commit(files or {'README.md': f'# {name}\n\nThe {name} repo.\n'}, 'Initial commit')
|
|
remotes[url] = path
|
|
return remote
|
|
|
|
with mock.patch('modules.repos.lib.ALLOWED_PROTOCOLS', ('https', 'file')), \
|
|
mock.patch('modules.repos.downloader.git_env', git_env):
|
|
yield factory
|
|
|
|
|
|
@pytest.fixture
|
|
def repos_download_manager(test_download_manager) -> DownloadManager:
|
|
test_download_manager.register_downloader(GitDownloader())
|
|
yield test_download_manager
|