T09 · Insecure Skill Coding Practices
- Location
scripts/validate_research_bundle.py:1163- Finding
Bundle Symlinks Permit Out-of-Scope File Reads and Arbitrary File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/validate_research_bundle.py:1163-1173, 1372-1379;scripts/research_review_validation.py:397-405
Vulnerability Type: Symlink following and incomplete bundle-boundary enforcement
Risk Level: MediumVulnerable Code
python for filename in REQUIRED_FILES: if not (bundle / filename).is_file(): errors.append(f"missing_required_file:{filename}") if errors: return { "status": "error", "research_status": "unknown", "review_status": "not_reviewed", "errors": errors, "warnings": warnings, "checks": checks, } request = read_text(bundle / "request_snapshot.md", errors) report = read_text(bundle / "final_report.md", errors) record = read_json(bundle / "research_record.json", errors)python def validate_independent_review( bundle: Path, question_ids: set[str], claim_ids: set[str], current_hashes: dict[str, str], errors: list[str], ) -> str: review_path = bundle / "independent_review.json" if not review_path.is_file(): return "not_reviewed" review = read_json(review_path, errors)python bundle_dir = args.bundle_dir.resolve() result = validate_bundle(bundle_dir) if bundle_dir.is_dir(): try: (bundle_dir / "validation_receipt.json").write_text( json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", )Technical Analysis
The validator treats the bundle directory as a filesystem security boundary, but it does not apply that boundary consistently. Python's
Path.is_file(),Path.read_text(), file opening, andPath.write_text()follow symbolic links by default.Consequently, attacker-controlled symlinks can be used for:
- Required candidate files such as
request_snapshot.md,final_report.md, andresearch_record.json. - The optional
independent_review.json. - The output file
validation_receipt.json.
...[truncated 2190 chars]
- Required candidate files such as
- Remediation
View remediation
Remediation Suggestions
Apply the bundle-boundary policy uniformly to every input and output:
-
Reject symbolic links for all fixed candidate files
- Use
lstat()orPath.is_symlink()before reading. - Require each candidate to be a regular file rather than merely accepting
is_file().
- Use
-
Resolve and verify every input path
- Resolve the bundle root once.
- Resolve each candidate and review path.
- Require every resolved path to satisfy
resolved_path.is_relative_to(bundle_root). - Reject paths that escape the bundle.
-
Secure the receipt destination
- Reject an existing
validation_receipt.jsonif it is a symbolic link or non-regular file. - Open the destination with no-follow semantics where supported, such as
os.open()withO_NOFOLLOW. - Write to a newly created temporary regular file inside the verified bundle directory, flush and synchronize it if required, and atomically replace the receipt.
- Verify the destination again immediately before replacement to reduce time-of-check/time-of-use risk.
- Reject an existing
-
Use a shared safe-path helper
- Centralize regular-file, symlink, resolution, and containment checks.
- Apply it to required files,
independent_review.json, source snapshots, candidate hashing, and receipt output.
-
Add regression tests
- Required-file symlink escaping the bundle.
- Review-file symlink escaping the bundle.
- Receipt symlink targeting an external file.
- Nested symlink traversal.
- Replacement races where practical.
A safe input check should follow this general pattern:
python def safe_regular_file(bundle_root: Path, relative_name: str) -> Path: path = bundle_root / relative_name if path.is_symlink(): raise ValueError("symbolic links are not allowed") resolved = path.resolve(strict=True) if not resolved.is_relative_to(bundle_root): raise ValueError("path escapes bundle") if not resolved.is_file(): raise ValueEr ...[truncated 163 chars]-
