T09 · Insecure Skill Coding Practices
- Location
scripts/undo.py:25- Finding
Crafted Undo Log Can Relocate Arbitrary Accessible Files
- Content
View full analysis
Vulnerability Details
File Location:
scripts/undo.py, lines 25-50 and 77-91
Vulnerability Type: Untrusted path injection and unrestricted file move
Risk Level: MediumTechnical Analysis
UndoManagertreats a user-supplied log file as an authoritative record of prior operations. It extracts source and target paths from any line containing the expected delimiters without validating the log's origin, constraining the paths to an organization directory, rejecting absolute paths, or resolving path traversal and symbolic links.The relevant parsing logic is:
python for line in lines: line = line.strip() if not line or line.startswith("#") or "整理统计:" in line: continue # Parse operation log if "移动:" in line: parts = line.split("移动:") if len(parts) > 1: op_parts = parts[1].strip().split("→") if len(op_parts) == 2: source = op_parts[0].strip() target = op_parts[1].strip() self.operations.append({ "type": "move", "source": source, "target": target, "original_line": line })During an undo operation, those fields are reversed and passed directly to
shutil.move:python if last_op["type"] == "move": source = last_op["target"] target = last_op["source"] source_path = Path(source) target_path = Path(target) if preview: print(f" 将移动: {source} → {target}") else: if source_path.exists(): # Ensure that the target directory exists target_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(source_path), str(target_path)) print(f" ✅ 已恢复: {source} → {target}") success = True else: print(f" ❌ 源文件不存在: {source}")Bec ...[truncated 2410 chars]
- Remediation
View remediation
Remediation Suggestions
-
Replace free-form text logs with a structured, versioned format such as JSON Lines. Treat every field as untrusted even after parsing.
-
Require an explicit organization root and constrain every undo path to that root:
python root = Path(args.path).resolve(strict=True) source_path = (root / logged_source).resolve(strict=True) target_path = (root / logged_target).resolve(strict=False) if not source_path.is_relative_to(root): raise ValueError("Source path escapes the organization root") if not target_path.is_relative_to(root): raise ValueError("Target path escapes the organization root") -
Store paths relative to the organization root. Reject absolute paths,
..components, null bytes, and paths that resolve outside the root. -
Detect and reject symbolic-link traversal. Validate resolved parent directories immediately before the move to reduce time-of-check/time-of-use risk.
-
Bind each log to a specific run and root directory. Where logs may cross trust boundaries, authenticate them with a keyed MAC or store them in a directory writable only by the executing user.
-
Record file identity information, such as size and a cryptographic hash, and verify it before undoing a move. This prevents a log entry from being applied to an unrelated file that later appeared at the same path.
-
Do not create arbitrary parent directories from log-controlled paths. Only recreate validated directories under the approved organization root.
-
Prompt for confirmation before moving files, displaying fully resolved source and destination paths. Retain
--previewas the recommended first step. -
Add negative tests covering absolute paths,
../traversal, symbolic links, logs modified after creation, destinations outside the root, and execution under elevated privileges.
-
