T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/rate-limiter.py:203
- Finding
- Predictable Temporary File Allows Symlink-Based File Truncation## Vulnerability Details **File Location**: `scripts/rate-limiter.py`, lines 203-210 **Vulnerability Type**: Predictable temporary file and unsafe symbolic-link handling **Risk Level**: Medium **Vulnerable Code**: ```python def save_state(state): STATE_PATH.parent.mkdir(parents=True, exist_ok=True) tmp = STATE_PATH.with_suffix(".tmp") # Write with restricted permissions (0o600) fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w") as f: f.write(json.dumps(state, indent=2) + "\n") tmp.rename(STATE_PATH) ``` ### Technical Analysis The state-saving operation always uses a predictable temporary path derived from `STATE_PATH`. For the default `rate-limit-state.json` state file, the temporary file is `rate-limit-state.tmp`. The temporary path is opened with `O_CREAT | O_TRUNC`, but without protections such as `O_EXCL` or `O_NOFOLLOW`. Consequently, if an attacker can write to the state directory, they can pre-create the temporary path as a symbolic link to another file. When the rate limiter next saves its state, `os.open()` follows that symbolic link, truncates the linked target, and writes JSON state data into it. The application-level lock does not prevent this attack because it coordinates cooperating rate-limiter processes through a separate `.lock` file; it does not stop another local process from creating or replacing the predictable temporary path. ### Attack Path 1. The attacker obtains write access to the directory containing the configured state file. 2. The attacker predicts the temporary filename by replacing the state file suffix with `.tmp`. 3. The attacker creates that path as a symbolic link to a target file writable by the rate-limiter process. 4. A user or automated process invokes any command that saves state, such as `gate`, `record`, `status`, `pause`, `resume`, `set-limit`, or `reset`. 5. `os.open()` follows the maliciou ...[truncated 978 chars]
- Remediation
- ## Remediation Suggestions - Create a randomized temporary file in the same directory as the destination by using `tempfile.NamedTemporaryFile` or `tempfile.mkstemp`. - Open the temporary file with exclusive-creation and symbolic-link protections where supported, including `O_CREAT`, `O_EXCL`, and `O_NOFOLLOW`. - Verify that the created object is a regular file with `os.fstat()` before writing. - Preserve restrictive permissions such as mode `0600`. - Flush Python buffers and call `os.fsync()` before replacement to improve crash safety. - Atomically install the completed state using `os.replace()`. - Optionally verify that the state directory is not writable by untrusted users. Example hardened implementation: ```python import tempfile import stat def save_state(state): STATE_PATH.parent.mkdir(parents=True, exist_ok=True) fd, tmp_name = tempfile.mkstemp( prefix=f".{STATE_PATH.name}.", suffix=".tmp", dir=str(STATE_PATH.parent), ) try: os.fchmod(fd, 0o600) file_info = os.fstat(fd) if not stat.S_ISREG(file_info.st_mode): raise OSError("Temporary state path is not a regular file") with os.fdopen(fd, "w") as f: fd = -1 f.write(json.dumps(state, indent=2) + "\n") f.flush() os.fsync(f.fileno()) os.replace(tmp_name, STATE_PATH) finally: if fd != -1: os.close(fd) try: os.unlink(tmp_name) except FileNotFoundError: pass ```
