T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/codex_auth.py:109
- Finding
- OAuth secrets are stored in predictably named temporary files without enforced restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/codex_auth.py:109-115`, with sensitive callers at `scripts/codex_auth.py:151-162` and `scripts/codex_auth.py:337-339` **Vulnerability Type**: Insecure temporary-file handling and plaintext credential storage **Risk Level**: High ### Vulnerable Code ```python def write_json_atomic(path, data): tmp = path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) f.write("\n") os.replace(tmp, path) ``` This helper is used to store pending PKCE credentials: ```python def save_pending(profile_id, verifier, state): pending = read_json(PENDING_PATH) pending[profile_id] = { "verifier": verifier, "state": state, "createdAt": int(time.time() * 1000) } os.makedirs(os.path.dirname(PENDING_PATH), exist_ok=True) write_json_atomic(PENDING_PATH, pending) ``` It is also used to store access and refresh tokens for queued application: ```python payload_path = f"/tmp/openclaw/codex-auth-apply-{profile_id.replace(':','_')}.json" write_json_atomic(payload_path, {"profile": profile_id, "tokens": tokens}) ``` ### Technical Analysis The temporary and destination files are created using the process's current `umask`; the code does not explicitly enforce mode `0600`. If the environment has a permissive `umask`, PKCE verifiers, OAuth state values, access tokens, and refresh tokens may be readable by other local users. The queued payload has a predictable filename derived from the profile ID. The generic writer also uses a predictable `path + ".tmp"` intermediate file and does not use exclusive creation, `O_NOFOLLOW`, ownership checks, or file-type validation. In a shared or insufficiently protected temporary directory, these properties create opportunities for local file disclosure, symlink attacks, and race conditions. Queued token payloads are not deleted after `apply_with_gateway_restart()` reads them, so refresh tokens ...[truncated 1365 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Store sensitive runtime state in a user-specific directory owned by the current user and set its mode to `0700`. - Create sensitive files with `os.open()` using `O_CREAT | O_EXCL | O_NOFOLLOW` and mode `0600`. - Generate cryptographically random queued-payload filenames rather than deriving them from profile IDs. - Reject symbolic links and verify the file owner, type, and permissions before reading or replacing a file. - Explicitly set the final file mode to `0600`, independent of the process `umask`. - Remove queued payloads in a `finally` block immediately after loading them. - Keep access and refresh tokens in memory where possible. - Use `fsync()` on the temporary file and parent directory when durability is required. ]]>
