T09 · Insecure Skill Coding Practices
- Location
scripts/freshness-check.py:101- Finding
Workspace Root Escape Allows Out-of-Scope File Reads
- Content
View full analysis
Vulnerability Details
File Location:
scripts/freshness-check.py, lines 101 and 131–132
Vulnerability Type: Path traversal and insufficient filesystem-boundary validation
Risk Level: MediumComplete Vulnerable Code Snippet
python def extract_timestamp(path: Path) -> tuple[str, datetime, str]: text = path.read_text(encoding="utf-8", errors="ignore") if path.suffix == ".json": try: data = json.loads(text) if isinstance(data, dict) and "updatedAt" in data: dt = parse_dt(str(data["updatedAt"])) if dt: return "updatedAt", dt, "json" except json.JSONDecodeError: pass head = "\n".join(text.splitlines()[:30]) for pattern in DATE_PATTERNS: m = pattern.search(head) if m: dt = parse_dt(m.group(1)) if dt: return "header-date", dt, "header" stat_dt = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).astimezone() return "mtime", stat_dt, "filesystem"python def check_one(root: Path, thresholds: dict[str, int], item: FreshnessItem) -> FreshnessResult: if item.group not in thresholds: raise SystemExit(f"ERR: unknown group in working set: {item.group}") full_path = (root / item.path).resolve() if not full_path.exists(): return FreshnessResult( group=item.group, path=item.path, thresholdDays=thresholds[item.group], source="missing", seenAt="", ageDays=999999.0, status="WARN", note="file missing", ) label, seen_at, source = extract_timestamp(full_path)Technical Analysis
The script accepts file paths from the configured
workingSetand combines each path with the operator-provided workspace root. Although the resulting path is resolved, the code does not verify that the resolved target remains beneath that root ...[truncated 2752 chars]- Remediation
View remediation
Remediation Suggestions
Enforce a strict workspace containment boundary before accessing any configured target:
python def resolve_workspace_file(root: Path, configured_path: str) -> Path: candidate = Path(configured_path) if candidate.is_absolute(): raise ValueError(f"Absolute paths are not allowed: {configured_path}") resolved_root = root.resolve(strict=True) resolved_target = (resolved_root / candidate).resolve(strict=True) if not resolved_target.is_relative_to(resolved_root): raise ValueError(f"Path escapes workspace root: {configured_path}") if not resolved_target.is_file(): raise ValueError(f"Target is not a regular file: {configured_path}") return resolved_targetAdditional hardening should include:
- Reject absolute paths and traversal outside the resolved root.
- Apply containment checks after symlink resolution so an in-workspace symlink cannot target an external file.
- Require regular files and reject directories, devices, FIFOs, and other special objects.
- Apply a reasonable maximum file-size limit before reading.
- Read only the bounded prefix required for header-date extraction instead of loading the entire file.
- For JSON inputs, either enforce a conservative size limit before parsing or use a bounded data format appropriate for freshness metadata.
- Treat invalid or escaping paths as explicit configuration errors rather than silently processing them.
- Add regression tests covering:
../traversal- absolute paths
- symlink escapes
- valid nested workspace files
- oversized files
- special filesystem objects
