diff --git a/gh-pages-template/index.html b/gh-pages-template/index.html index 4a150a2afe4..4963070625a 100644 --- a/gh-pages-template/index.html +++ b/gh-pages-template/index.html @@ -42,7 +42,8 @@
- +
diff --git a/src/queue_eligibility.py b/src/queue_eligibility.py index 8c21dd5652d..b2c40a7db69 100644 --- a/src/queue_eligibility.py +++ b/src/queue_eligibility.py @@ -52,7 +52,48 @@ def normalize_user_id(user_id: object) -> str: return str(user_id).strip() -def load_auto_approved_user_ids(auto_approved_users_file: Path) -> frozenset[str]: +def resolve_auto_approved_users_file( + auto_approved_users_file: Path, + base_dir: Path | None = None, +) -> Path: + """ + Resolve an auto-approved users file path within the allowed base directory. + + Parameters + ---------- + auto_approved_users_file : pathlib.Path + Candidate JSON file path. + base_dir : pathlib.Path, optional + Directory that may contain the allowlist file. + + Returns + ------- + pathlib.Path + Canonical allowlist file path. + + Raises + ------ + ValueError + If the resolved path escapes the allowed base directory. + """ + allowed_base_dir = (Path.cwd() if base_dir is None else base_dir).resolve() + candidate_file = auto_approved_users_file + if not candidate_file.is_absolute(): + candidate_file = allowed_base_dir / candidate_file + + resolved_file = candidate_file.resolve() + try: + resolved_file.relative_to(allowed_base_dir) + except ValueError as error: + raise ValueError('auto-approved users file must be inside the working directory') from error + + return resolved_file + + +def load_auto_approved_user_ids( + auto_approved_users_file: Path, + base_dir: Path | None = None, +) -> frozenset[str]: """ Load auto-approved GitHub user ids from a JSON file. @@ -60,6 +101,8 @@ def load_auto_approved_user_ids(auto_approved_users_file: Path) -> frozenset[str ---------- auto_approved_users_file : pathlib.Path JSON file containing auto-approved user objects. + base_dir : pathlib.Path, optional + Directory that may contain the allowlist file. Returns ------- @@ -67,9 +110,13 @@ def load_auto_approved_user_ids(auto_approved_users_file: Path) -> frozenset[str Normalized GitHub user ids. """ try: - with auto_approved_users_file.open(encoding='utf-8') as auto_approved_users_f: + safe_auto_approved_users_file = resolve_auto_approved_users_file( + auto_approved_users_file=auto_approved_users_file, + base_dir=base_dir, + ) + with safe_auto_approved_users_file.open(encoding='utf-8') as auto_approved_users_f: auto_approved_users = json.load(auto_approved_users_f) - except (OSError, json.JSONDecodeError): + except (OSError, ValueError): return frozenset() if not isinstance(auto_approved_users, list): diff --git a/tests/unit/test_queue_eligibility.py b/tests/unit/test_queue_eligibility.py index 37d5cdf5e3c..5734708b8d6 100644 --- a/tests/unit/test_queue_eligibility.py +++ b/tests/unit/test_queue_eligibility.py @@ -30,6 +30,7 @@ def test_load_auto_approved_user_ids_loads_user_ids(tmp_path): auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( auto_approved_users_file=auto_approved_users_file, + base_dir=tmp_path, ) assert auto_approved_user_ids == frozenset({'42013603', '88998541'}) @@ -61,6 +62,7 @@ def test_load_auto_approved_user_ids_ignores_malformed_entries(tmp_path): auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( auto_approved_users_file=auto_approved_users_file, + base_dir=tmp_path, ) assert auto_approved_user_ids == frozenset({'1234'}) @@ -71,8 +73,14 @@ def test_load_auto_approved_user_ids_fails_closed_for_missing_or_bad_file(tmp_pa bad_file = tmp_path / 'bad.json' bad_file.write_text('{', encoding='utf-8') - assert queue_eligibility.load_auto_approved_user_ids(auto_approved_users_file=missing_file) == frozenset() - assert queue_eligibility.load_auto_approved_user_ids(auto_approved_users_file=bad_file) == frozenset() + assert queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=missing_file, + base_dir=tmp_path, + ) == frozenset() + assert queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=bad_file, + base_dir=tmp_path, + ) == frozenset() def test_load_auto_approved_user_ids_fails_closed_for_non_list_json(tmp_path): @@ -82,6 +90,25 @@ def test_load_auto_approved_user_ids_fails_closed_for_non_list_json(tmp_path): auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( auto_approved_users_file=auto_approved_users_file, + base_dir=tmp_path, + ) + + assert auto_approved_user_ids == frozenset() + + +def test_load_auto_approved_user_ids_rejects_path_outside_base_dir(tmp_path): + base_dir = tmp_path / 'base' + base_dir.mkdir() + outside_file = write_auto_approved_users_file(tmp_path, [ + { + 'user_id': 42013603, + 'username': 'ReenigneArcher', + }, + ]) + + auto_approved_user_ids = queue_eligibility.load_auto_approved_user_ids( + auto_approved_users_file=Path('..') / outside_file.name, + base_dir=base_dir, ) assert auto_approved_user_ids == frozenset() @@ -213,10 +240,11 @@ def test_main_writes_queue_eligible_result(tmp_path, monkeypatch): ]) output_file = tmp_path / 'github_output' monkeypatch.setenv('GITHUB_OUTPUT', str(output_file)) + monkeypatch.chdir(tmp_path) monkeypatch.setattr(sys, 'argv', [ 'queue_eligibility', '--auto-approved-users-file', - str(auto_approved_users_file), + auto_approved_users_file.name, '--user-id', '42013603', ])