T09 · Insecure Skill Coding Practices
- Location
scripts/log_expense.py:11- Finding
Path Traversal Through Unvalidated Date and Month Values
- Content
View full analysis
Vulnerability Details
File Location:
scripts/log_expense.py:11-22, with attacker-controlled values propagated atscripts/log_expense.py:35-40andscripts/log_expense.py:117-134
Vulnerability Type: Path traversal and unintended file access
Risk Level: HighVulnerable Code
python def get_expense_file(year_month: str, workspace_path: str = None) -> Path: """Get the path to the expense file for a given month.""" if workspace_path is None: workspace_path = Path.home() / ".openclaw" / "workspace" else: workspace_path = Path(workspace_path) expenses_dir = workspace_path / "expenses" expenses_dir.mkdir(exist_ok=True) return expenses_dir / f"{year_month}.md"The unvalidated path component is populated from both logging and summary arguments:
python def log_expense(amount: float, category: str, description: str = "", tags: str = "", date: str = None, workspace_path: str = None): """Log an expense entry.""" if date is None: date = datetime.now().strftime("%Y-%m-%d") year_month = date[:7] # YYYY-MM file_path = get_expense_file(year_month, workspace_path)python summary_parser.add_argument("year_month", nargs="?", help="Year-month (YYYY-MM, defaults to current month)") summary_parser.add_argument("--workspace", help="Workspace path") summary_parser.add_argument("--json", action="store_true", help="Output as JSON") ... year_month = args.year_month or datetime.now().strftime("%Y-%m") summary = get_monthly_summary(year_month, args.workspace)Technical Analysis
get_expense_file()concatenatesyear_monthdirectly into a filesystem path without validating that it conforms toYYYY-MM. It also does not resolve the resulting path and verify that it remains inside the intendedexpensesdirectory.The
summarycommand acceptsyear_monthdirectl ...[truncated 2086 chars]- Remediation
View remediation
Remediation Suggestions
- Validate logging dates by parsing them with
datetime.strptime(date, "%Y-%m-%d"). - Validate summary values with
datetime.strptime(year_month, "%Y-%m"). - Regenerate canonical date and month strings from parsed datetime objects instead of retaining the original input.
- Resolve both the expense directory and candidate file path before access.
- Verify that the resolved candidate is a direct child of the resolved expense directory.
- Reject path separators,
.., absolute paths, malformed dates, and unexpected suffixes. - Consider restricting
--workspaceto an explicitly approved root when arguments may originate from untrusted users.
Example hardening pattern:
python def get_expense_file(year_month: str, workspace_path: str = None) -> Path: parsed = datetime.strptime(year_month, "%Y-%m") canonical_month = parsed.strftime("%Y-%m") workspace = ( Path(workspace_path).expanduser().resolve() if workspace_path else (Path.home() / ".openclaw" / "workspace").resolve() ) expenses_dir = (workspace / "expenses").resolve() expenses_dir.mkdir(parents=True, exist_ok=True) candidate = (expenses_dir / f"{canonical_month}.md").resolve() if candidate.parent != expenses_dir: raise ValueError("Expense path is outside the approved directory") return candidate- Validate logging dates by parsing them with
