T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/bicqa.py:191
- Finding
- Predictable Temporary State File with Delayed Permission Hardening## Vulnerability Details **File Location**: `scripts/bicqa.py:191-197` **Vulnerability Type**: Predictable temporary file, symlink following, and transient insecure permissions **Risk Level**: Medium ### Vulnerable Code ```python def save_state(state): p = resolve_state_path() p.parent.mkdir(parents=True, exist_ok=True) tmp = p.with_suffix(p.suffix + ".tmp") tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8") os.replace(tmp, p) try: os.chmod(p, 0o600) except OSError: pass ``` The state can contain recent user input and session identifiers, as shown at `scripts/bicqa.py:474-478`: ```python rec["last_used"] = time.time() rec["turns"] = int(rec.get("turns") or 0) + 1 rec["last_question"] = args.q[:200] if result["tokens"]: rec["tokens"] = int(rec.get("tokens") or 0) + int(result["tokens"]) save_state(state) ``` ### Technical Analysis `save_state()` constructs a predictable temporary pathname by appending `.tmp` to the configured state filename. It then opens that pathname through `Path.write_text()`, which does not request exclusive creation and follows symbolic links. The temporary file initially receives permissions derived from the process umask. Mode `0600` is applied only to the final destination after `os.replace()`. Consequently, if the configured state directory is shared or attacker-writable, a local attacker may: 1. Read the temporary file before replacement if the effective umask permits it. 2. Pre-create the predictable temporary pathname as a symbolic link. 3. Cause `write_text()` to truncate and overwrite the symlink target if the victim process has permission to write to it. The environment-controlled `BICQA_STATE_FILE` and `BICQA_STATE_DIR` settings make deployment into a shared or otherwise unsafe directory possible. Exploitation therefore requires local access to the selected directory and is not remotely achievable through the BIC-QA API alone. ### Attack Pat ...[truncated 1341 chars]
- Remediation
- ## Remediation Suggestions 1. Create the temporary file atomically and exclusively in the destination directory using `tempfile.mkstemp()` or an equivalent secure primitive. 2. Set mode `0600` when creating the temporary file rather than after replacement. 3. Write through the returned file descriptor, flush buffered data, and call `os.fsync()` before performing `os.replace()`. 4. Reject pre-existing symbolic links and avoid reopening the temporary file by pathname after secure creation. 5. Verify that the state directory is owned by the current user and is not group- or world-writable. Reject unsafe custom state locations unless explicitly overridden with a documented warning. 6. Apply restrictive permissions to the state directory, such as `0700`, where supported. 7. Preserve cleanup logic so the temporary file is removed if serialization, writing, synchronization, or replacement fails. A secure implementation should follow this pattern: ```python import tempfile def save_state(state): p = resolve_state_path() p.parent.mkdir(parents=True, exist_ok=True, mode=0o700) fd, tmp_name = tempfile.mkstemp( prefix=p.name + ".", suffix=".tmp", dir=str(p.parent), ) try: os.fchmod(fd, 0o600) data = json.dumps(state, ensure_ascii=False, indent=2).encode("utf-8") with os.fdopen(fd, "wb") as tmp_file: fd = -1 tmp_file.write(data) tmp_file.flush() os.fsync(tmp_file.fileno()) os.replace(tmp_name, p) os.chmod(p, 0o600) finally: if fd != -1: os.close(fd) try: os.unlink(tmp_name) except FileNotFoundError: pass ```
