T09 · Insecure Skill Coding Practices
- Location
logger.py:4- Finding
Unrestricted File Write Through Caller-Controlled Log Paths
- Content
View full analysis
Vulnerability Details
File Location:
logger.py, lines 4–10
Vulnerability Type: Arbitrary file append, overwrite, and path traversal
Risk Level: MediumVulnerable Code
python def log_journal(entry, path="../logs/journal.log"): with open(path, "a") as f: f.write(f"[{datetime.now()}] {entry}\n") def log_actions(actions, path="../logs/actions.json"): with open(path, "w") as f: json.dump(actions, f, indent=2)Technical Analysis
Both public logging functions accept a caller-supplied filesystem path and pass it directly to
open()without validation, canonicalization, or restriction to an approved logging directory.log_journal()opens the selected file in append mode, whilelog_actions()opens it in write mode. The latter truncates an existing target before writing JSON. Absolute paths and relative traversal sequences such as../../targetare accepted.The default paths also contain
..and are resolved relative to the process's current working directory rather than the module directory. Consequently, even default behavior may place logs in an unintended location depending on how the skill is launched.Exploitation requires an integration or caller that exposes or derives the
pathargument from attacker-controlled input. The reviewed internal calls do not pass a custom path, so the practical exposure depends on how these public functions are integrated.Attack Path
- An attacker supplies input that influences a logging destination exposed by an integrating agent or application.
- The attacker chooses an absolute path or a traversal path such as
../../target-file. - The integration passes that value to
log_journal()orlog_actions(). - Python resolves the path outside the intended log directory.
log_journal()appends attacker-influenced content to the target, orlog_actions()truncates and replaces it with serialized JSON.- The operation succeeds for any target writable ...[truncated 600 chars]
- Remediation
View remediation
Remediation Suggestions
- Remove the public
pathparameter when callers do not need to select a destination. - Define a fixed log directory from a trusted configuration source and resolve it independently of the current working directory.
- If selectable filenames are required, accept only a basename or a strict allowlist of known log names.
- Canonicalize the requested destination with
pathlib.Path.resolve()and verify that it remains beneath the approved log directory usingPath.is_relative_to(). - Reject absolute paths, traversal components, symbolic-link escapes, and unsupported file extensions.
- Create the log directory with restrictive permissions and run the skill under a least-privileged account.
- Use atomic replacement for action logs where replacement is required, and avoid truncating existing files until the new content has been safely serialized.
- Consider opening files with protections against symbolic-link following where supported.
A hardened pattern is:
python import json from pathlib import Path LOG_DIR = (Path(__file__).resolve().parent / "logs").resolve() LOG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True) def _safe_log_path(filename): if Path(filename).name != filename: raise ValueError("Log filename must not contain path components") destination = (LOG_DIR / filename).resolve() if not destination.is_relative_to(LOG_DIR): raise ValueError("Log path escapes the approved directory") return destination def log_journal(entry): destination = _safe_log_path("journal.log") with destination.open("a", encoding="utf-8") as file: file.write(f"{entry}\n") def log_actions(actions): destination = _safe_log_path("actions.json") temporary = _safe_log_path("actions.json.tmp") with temporary.open("w", encoding="utf-8") as file: json.dump(actions, file, indent=2) temporary.replace(destination)- Remove the public
