T09 · Insecure Skill Coding Practices
- Location
scripts/organize.py:116- Finding
Untrusted Undo History Allows Arbitrary File Moves and Log Deletion
- Content
View full analysis
Vulnerability Details
File Location:
scripts/organize.py, lines 116–135
Vulnerability Type: Unvalidated path usage from an untrusted history file
Risk Level: Mediumpython def undo(self, log_file): if not os.path.exists(log_file): logger.error("Log file not found.") return with open(log_file, 'r') as f: history = json.load(f) logger.info(f"Undoing {len(history)} operations...") for item in reversed(history): src = item['src'] dst = item['dst'] try: if os.path.exists(dst): shutil.move(dst, src) logger.info(f"Restored: {os.path.basename(dst)} -> {os.path.dirname(src)}") except Exception as e: logger.error(f"Failed to restore {dst}: {e}") os.remove(log_file) logger.info("Undo complete.")Technical Analysis
The
--undooption accepts an arbitrary JSON file and directly uses its attacker-controlledsrcanddstvalues as arguments toshutil.move(). The implementation does not:- Validate the structure or types of history entries.
- Canonicalize paths before using them.
- Require paths to remain beneath the directory that was organized.
- Verify that the history was generated by this application.
- Reject symbolic links or path traversal.
- Check for destination conflicts before restoration.
- Preserve the history file when one or more operations fail.
Consequently, a crafted history entry can select any existing file accessible to the process as
dstand any writable path assrc.shutil.move(dst, src)will then relocate that file. Depending on platform and destination semantics, an existing destination may also be replaced or the selected file may be moved into an attacker-selected directory.The history file is unconditionally deleted after processing, including when restoration operations fail. This can re ...[truncated 1593 chars]
- Remediation
View remediation
Remediation Suggestions
- Validate the loaded JSON against a strict schema:
- Require the root value to be a list.
- Require each entry to contain only string-valued
srcanddstfields. - Reject malformed, missing, or unexpected fields.
- Store the organization root and a format version in the history file.
- Resolve every path with
os.path.realpath()and verify, usingos.path.commonpath(), that both source and destination remain under the recorded organization root. - Reject entries containing paths outside that root, including paths that escape through symbolic links.
- Consider refusing history files not generated by the application. Where authenticity is required, protect history files with restrictive permissions or a keyed integrity check.
- Before moving a file, verify that the current file state matches the recorded operation and refuse destination conflicts rather than implicitly replacing or nesting files.
- Use a two-phase undo process: validate every entry before performing any move, then execute only if the complete history is valid.
- Delete or archive the history file only after all operations succeed. Preserve it and return a nonzero exit status when an undo is incomplete.
- Add a dry-run option for undo so users can review all resolved source and destination paths before filesystem changes occur.
- Validate the loaded JSON against a strict schema:
