From 37cf5bdb489090d73200fe999587e79977f60f2b Mon Sep 17 00:00:00 2001 From: beyildirim <101638632+beyildirim@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:13:34 +0200 Subject: [PATCH 1/4] Fix verifier path traversal in lab runtime --- tests/platform/test_lab_runtime.py | 10 ++++++++++ weaklink_platform/lab_runtime.py | 19 ++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/platform/test_lab_runtime.py b/tests/platform/test_lab_runtime.py index 720308b..a744a22 100644 --- a/tests/platform/test_lab_runtime.py +++ b/tests/platform/test_lab_runtime.py @@ -39,6 +39,16 @@ def test_execute_lab_verifier_rejects_shell_only_lab(tmp_path: Path) -> None: assert result.error == "No verify.py for lab 9.8" +def test_execute_lab_verifier_rejects_lab_path_escape(tmp_path: Path) -> None: + labs_root = tmp_path / "labs" + labs_root.mkdir() + + result = execute_lab_verifier("../escape", labs_root=labs_root) + + assert result.passed is False + assert result.error == "Invalid lab path for lab ../escape" + + def test_load_lab_manifest_supports_response_phase_shape() -> None: manifest = load_lab_manifest( Path("labs/tier-7-detection-response/7.5-threat-modeling") diff --git a/weaklink_platform/lab_runtime.py b/weaklink_platform/lab_runtime.py index 8895356..ef951d0 100644 --- a/weaklink_platform/lab_runtime.py +++ b/weaklink_platform/lab_runtime.py @@ -226,6 +226,13 @@ def read_env_exports(path: Path) -> dict[str, str]: return env +def _resolve_descendant(root: Path, child: str | Path) -> Path: + resolved_root = root.resolve(strict=False) + resolved_child = (resolved_root / child).resolve(strict=False) + resolved_child.relative_to(resolved_root) + return resolved_child + + def main_init(callback: InitHook, argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--json", action="store_true") @@ -386,10 +393,16 @@ def execute_lab_verifier( labs_root: Path = DEFAULT_LABS_ROOT, timeout: int = 30, ) -> VerificationResult: - resolved_lab_dir = lab_dir or (labs_root / lab_id) - env = _verifier_env(lab_id, resolved_lab_dir) + try: + if lab_dir is not None: + resolved_lab_dir = _resolve_descendant(lab_dir.parent, lab_dir.name) + else: + resolved_lab_dir = _resolve_descendant(labs_root, lab_id) + python_verifier = _resolve_descendant(resolved_lab_dir, "verify.py") + except ValueError: + return VerificationResult(False, (), error=f"Invalid lab path for lab {lab_id}") - python_verifier = resolved_lab_dir / "verify.py" + env = _verifier_env(lab_id, resolved_lab_dir) try: if python_verifier.exists(): From fcfed6513defcc30d6f4c92693ae3c67285de860 Mon Sep 17 00:00:00 2001 From: beyildirim <101638632+beyildirim@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:20:22 +0200 Subject: [PATCH 2/4] Tighten lab id validation for verifier path --- weaklink_platform/lab_runtime.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/weaklink_platform/lab_runtime.py b/weaklink_platform/lab_runtime.py index ef951d0..1991a34 100644 --- a/weaklink_platform/lab_runtime.py +++ b/weaklink_platform/lab_runtime.py @@ -4,6 +4,7 @@ import base64 import json import os +import re import shlex import subprocess from dataclasses import dataclass, field @@ -19,6 +20,7 @@ DEFAULT_REPOS_ROOT = Path("/repos") DEFAULT_WORKSPACE_ROOT = Path("/workspace") DEFAULT_LABS_ROOT = Path("/opt/labs") +LAB_ID_PATTERN = re.compile(r"\d+\.\d+") def _package_root() -> Path: @@ -226,11 +228,10 @@ def read_env_exports(path: Path) -> dict[str, str]: return env -def _resolve_descendant(root: Path, child: str | Path) -> Path: - resolved_root = root.resolve(strict=False) - resolved_child = (resolved_root / child).resolve(strict=False) - resolved_child.relative_to(resolved_root) - return resolved_child +def _validated_lab_id(lab_id: str) -> str: + if not LAB_ID_PATTERN.fullmatch(lab_id): + raise ValueError(f"Invalid lab id: {lab_id}") + return lab_id def main_init(callback: InitHook, argv: list[str] | None = None) -> int: @@ -395,10 +396,14 @@ def execute_lab_verifier( ) -> VerificationResult: try: if lab_dir is not None: - resolved_lab_dir = _resolve_descendant(lab_dir.parent, lab_dir.name) + resolved_lab_dir = lab_dir.resolve(strict=False) else: - resolved_lab_dir = _resolve_descendant(labs_root, lab_id) - python_verifier = _resolve_descendant(resolved_lab_dir, "verify.py") + resolved_labs_root = labs_root.resolve(strict=False) + safe_lab_id = _validated_lab_id(lab_id) + resolved_lab_dir = (resolved_labs_root / safe_lab_id).resolve(strict=False) + resolved_lab_dir.relative_to(resolved_labs_root) + python_verifier = (resolved_lab_dir / "verify.py").resolve(strict=False) + python_verifier.relative_to(resolved_lab_dir) except ValueError: return VerificationResult(False, (), error=f"Invalid lab path for lab {lab_id}") From 9b7eb55e56054344b2707f850f2387cbd1389e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Ekin=20Y=C4=B1ld=C4=B1r=C4=B1m?= <101638632+beyildirim@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:24:46 +0200 Subject: [PATCH 3/4] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- weaklink_platform/lab_runtime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/weaklink_platform/lab_runtime.py b/weaklink_platform/lab_runtime.py index 1991a34..728a39d 100644 --- a/weaklink_platform/lab_runtime.py +++ b/weaklink_platform/lab_runtime.py @@ -395,13 +395,13 @@ def execute_lab_verifier( timeout: int = 30, ) -> VerificationResult: try: + resolved_labs_root = labs_root.resolve(strict=False) if lab_dir is not None: resolved_lab_dir = lab_dir.resolve(strict=False) else: - resolved_labs_root = labs_root.resolve(strict=False) safe_lab_id = _validated_lab_id(lab_id) resolved_lab_dir = (resolved_labs_root / safe_lab_id).resolve(strict=False) - resolved_lab_dir.relative_to(resolved_labs_root) + resolved_lab_dir.relative_to(resolved_labs_root) python_verifier = (resolved_lab_dir / "verify.py").resolve(strict=False) python_verifier.relative_to(resolved_lab_dir) except ValueError: From 2da0928596cbd72367f5216095c062422ac7888e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Ekin=20Y=C4=B1ld=C4=B1r=C4=B1m?= <101638632+beyildirim@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:37:20 +0200 Subject: [PATCH 4/4] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- weaklink_platform/lab_runtime.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/weaklink_platform/lab_runtime.py b/weaklink_platform/lab_runtime.py index 728a39d..738c8a4 100644 --- a/weaklink_platform/lab_runtime.py +++ b/weaklink_platform/lab_runtime.py @@ -396,11 +396,13 @@ def execute_lab_verifier( ) -> VerificationResult: try: resolved_labs_root = labs_root.resolve(strict=False) + safe_lab_id = _validated_lab_id(lab_id) + expected_lab_dir = (resolved_labs_root / safe_lab_id).resolve(strict=False) if lab_dir is not None: - resolved_lab_dir = lab_dir.resolve(strict=False) - else: - safe_lab_id = _validated_lab_id(lab_id) - resolved_lab_dir = (resolved_labs_root / safe_lab_id).resolve(strict=False) + provided_lab_dir = lab_dir.resolve(strict=False) + if provided_lab_dir != expected_lab_dir: + raise ValueError(f"Lab directory does not match lab id: {lab_id}") + resolved_lab_dir = expected_lab_dir resolved_lab_dir.relative_to(resolved_labs_root) python_verifier = (resolved_lab_dir / "verify.py").resolve(strict=False) python_verifier.relative_to(resolved_lab_dir)