T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/invoice.py:291
- Finding
- Path Traversal in Invoice Payment Status Update<![CDATA[ ## Vulnerability Details **File Location**: `scripts/invoice.py`, lines 291–299 **Vulnerability Type**: Path traversal with unauthorized JSON file modification **Risk Level**: Medium ### Vulnerable Code ```python def mark_paid(args): path = DATA_PATH / f"{args.id}.json" if not path.exists(): print(f"Invoice {args.id} not found.") return inv = json.loads(path.read_text()) inv["status"] = "paid" inv["paidDate"] = datetime.now().strftime("%Y-%m-%d") path.write_text(json.dumps(inv, indent=2)) print(f"Invoice {args.id} marked as paid.") ``` ### Technical Analysis The `paid` command accepts an invoice identifier through the `--id` argument. This value is incorporated directly into a filesystem path without validating that it has the expected invoice-number format or ensuring that the resolved path remains inside `DATA_PATH`. Directory traversal sequences such as `../` can escape the intended invoice directory. An absolute path can also replace the base path under standard `pathlib` path-joining behavior. If the selected target exists and contains a JSON object, the application reads it, adds or replaces the `status` and `paidDate` properties, and rewrites the entire file. Exploitation is constrained to files that are accessible to the current operating-system user and contain JSON that deserializes to an object. Nevertheless, the command crosses the intended invoice-storage boundary and can corrupt unrelated application state. ### Attack Path 1. The attacker or untrusted caller invokes the skill's `paid` command. 2. The attacker supplies a traversal-based identifier, such as: ```bash python3 scripts/invoice.py paid --id ../target ``` 3. The resulting path resolves to `~/.openclaw/target.json` rather than a file beneath `~/.openclaw/invoices`. 4. If the target exists and contains a JSON object, the program loads it. 5. The program inserts or overwrites the `status` and `paidDate` fields. 6. The mo ...[truncated 847 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Validate invoice identifiers against a strict allowlist pattern before constructing a path: ```python import re if not re.fullmatch(r"INV-\d{4}-\d{3,}", args.id): raise ValueError("Invalid invoice identifier") ``` 2. Resolve both the data directory and candidate path, then verify containment: ```python base = DATA_PATH.resolve() path = (base / f"{args.id}.json").resolve() if path.parent != base: raise ValueError("Invoice path is outside the data directory") ``` 3. Reject identifiers containing path separators, traversal components, or absolute paths. 4. Validate that loaded data is a dictionary before modifying it: ```python inv = json.loads(path.read_text()) if not isinstance(inv, dict): raise ValueError("Invalid invoice record") ``` 5. Add tests for `../`, absolute paths, nested paths, encoded traversal forms, malformed identifiers, invalid JSON, and JSON values that are not objects. ]]>
