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',
])