T09 · Insecure Skill Coding Practices
- Location
board_skill/cli.py:50- Finding
Predictable Temporary File Allows Symlink-Based Arbitrary File Overwrite
- Content
View full analysis
None: """Write content to a file atomically via temp-and-rename. On POSIX systems ``os.replace`` is atomic within the same filesystem, preventing partial writes from corrupting the target file. """ tmp = path.with_suffix(path.suffix + ".tmp") try: tmp.write_text(content, encoding="utf-8") os.replace(str(tmp), str(path)) except BaseException: tmp.unlink(missing_ok=True) raise ``` ### Technical Analysis The temporary path is derived deterministically from the destination path by appending `.tmp`. The code neither creates the temporary file with an exclusive-create operation nor verifies that an existing path is not a symbolic link. `Path.write_text()` opens the selected path through normal filesystem semantics and therefore follows symbolic links. An attacker who can write to the output directory can create the predictable temporary path as a symbolic link to another file before the CLI runs. When `_atomic_write()` executes, it follows that link and truncates or overwrites the linked target with generated content. The subsequent `os.replace()` does not prevent the initial overwrite. It atomically renames the symlink directory entry over the requested destination only after data has already been written through the symlink. This helper is used by the `compile` command for ordinary and SOUL output and by the `init` command, making the issue reachable through supported CLI operations. ### Attack Path 1. The attacker identifies the output path that a victim will use, such as `/shared/result.json`. 2. The attacker has write access to that output directory and creates the predictable path `/shared/result.json.tmp`. 3. The attacker ...[truncated 1538 chars]- Remediation
View remediation
None: path = Path(path) fd, temporary_name = tempfile.mkstemp( prefix=f".{path.name}.", suffix=".tmp", dir=path.parent, text=True, ) temporary_path = Path(temporary_name) try: os.chmod(temporary_path, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(content) handle.flush() os.fsync(handle.fileno()) os.replace(temporary_path, path) except BaseException: try: os.close(fd) except OSError: pass temporary_path.unlink(missing_ok=True) raise ``` ]]>
