Skip to content

Commit e938b5e

Browse files
committed
fix CodeQL alert
1 parent 1bbbbb9 commit e938b5e

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

app/scenario_planning.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,21 @@ def _normalize_pack_id(value: Any) -> str:
3333
def _resolve_workspace_path(candidate: Path) -> Path | None:
3434
try:
3535
workspace_root = WORKSPACE_ROOT.resolve()
36-
raw_path = candidate if candidate.is_absolute() else (workspace_root / candidate)
37-
resolved = raw_path.resolve(strict=False)
36+
base_str = os.path.abspath(str(workspace_root))
37+
if not base_str.endswith(os.sep):
38+
base_str += os.sep
39+
40+
raw_str = str(candidate)
41+
if os.path.isabs(raw_str):
42+
target_str = os.path.abspath(raw_str)
43+
else:
44+
target_str = os.path.abspath(os.path.join(base_str, raw_str))
45+
46+
if not target_str.startswith(base_str) and target_str != base_str.rstrip(os.sep):
47+
return None
48+
49+
# Fully contained lexically, now safe to resolve symlinks
50+
resolved = Path(target_str).resolve(strict=False)
3851
resolved.relative_to(workspace_root)
3952
return resolved
4053
except (OSError, RuntimeError, ValueError):
@@ -72,10 +85,8 @@ def _read_json_file(*, base_dir: Path, pack_id: str, filename: str) -> dict[str,
7285
if not safe_pack_id:
7386
return None
7487

75-
resolved_path = (base_path / safe_pack_id / filename).resolve(strict=False)
76-
try:
77-
resolved_path.relative_to(base_path)
78-
except ValueError:
88+
resolved_path = _resolve_workspace_path(base_path / safe_pack_id / filename)
89+
if resolved_path is None:
7990
return None
8091

8192
if not resolved_path.exists() or not resolved_path.is_file():
@@ -603,7 +614,7 @@ def _render_flow_fallback(payloads: dict[str, Any]) -> None:
603614
data_root=data_root,
604615
output_root=output_root,
605616
output_pack_id=_normalize_pack_id(output_pack_override.strip()) or None,
606-
)
617+
) or {}
607618
paths = dict(bundle.get("paths") or {})
608619
payloads = dict(bundle.get("payloads") or {})
609620
slot_labels = _load_scenario_slot_labels()

src/omen/scenario/loader.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
import os
67
from pathlib import Path
78
from typing import Any
89

@@ -17,6 +18,17 @@ def _read_json(path: Path) -> dict[str, Any] | None:
1718
return payload if isinstance(payload, dict) else None
1819

1920

21+
def _is_safe_subpath(base: Path, subpath: str) -> bool:
22+
"""Validate that the given subpath resolves strictly within the base directory."""
23+
if not subpath:
24+
return False
25+
base_str = os.path.abspath(str(base))
26+
if not base_str.endswith(os.sep):
27+
base_str += os.sep
28+
target_str = os.path.abspath(os.path.join(base_str, str(subpath)))
29+
return target_str.startswith(base_str)
30+
31+
2032
def discover_spec8_pack_candidates(
2133
data_root: str | Path = "data/scenarios",
2234
output_root: str | Path = "output",
@@ -56,9 +68,13 @@ def load_spec8_flow_artifacts(
5668
data_root: str | Path = "data/scenarios",
5769
output_root: str | Path = "output",
5870
output_pack_id: str | None = None,
59-
) -> dict[str, Any]:
71+
) -> dict[str, Any] | None:
6072
data_base = Path(data_root)
6173
output_base = Path(output_root)
74+
75+
if not _is_safe_subpath(data_base, pack_id):
76+
return None
77+
6278
data_pack = data_base / pack_id
6379

6480
situation = _read_json(data_pack / "situation.json")
@@ -69,6 +85,8 @@ def load_spec8_flow_artifacts(
6985
generation_trace = _read_json(data_pack / "generation" / "log.json")
7086

7187
resolved_output_pack = output_pack_id or pack_id
88+
if not _is_safe_subpath(output_base, resolved_output_pack):
89+
return None
7290
result = _read_json(output_base / resolved_output_pack / "result.json")
7391
explanation = _read_json(output_base / resolved_output_pack / "explanation.json")
7492

0 commit comments

Comments
 (0)