T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/slides.py:21
- Finding
- OAuth Refresh Token Exported to a Predictable Persistent Temporary File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/slides.py`, lines 21–39 **Vulnerability Type**: Unsafe temporary-file handling and plaintext credential exposure **Risk Level**: High ### Vulnerable Code ```python GOG_CREDS = os.path.expanduser("~/.config/gogcli/credentials.json") TOKEN_TMP = "/tmp/gog_slides_token.json" ACCOUNT = os.environ.get("GOG_ACCOUNT", "david@hml.tech") def get_creds(): from google.oauth2.credentials import Credentials from google.auth.transport.requests import Request result = subprocess.run( ["gog", "auth", "tokens", "export", ACCOUNT, "--out", TOKEN_TMP, "--overwrite"], capture_output=True, text=True ) if result.returncode != 0: print(f"Error exporting token: {result.stderr}", file=sys.stderr) sys.exit(1) with open(TOKEN_TMP) as f: token_data = json.load(f) ``` ### Technical Analysis The script exports an OAuth refresh token to the fixed path `/tmp/gog_slides_token.json`. This filename is predictable, located in a commonly shared temporary directory, and reused with `--overwrite`. The script does not create the file itself with an explicitly restrictive mode, verify that the destination is a regular file owned by the current user, reject symbolic links, or remove the token file after loading it. The actual file permissions and overwrite protections may depend on the behavior of the external `gog` command, but this script does not independently enforce them. A refresh token is long-lived credential material. Leaving it on disk after the process completes unnecessarily expands the time during which another local process, another user where permissions permit, malware, backup software, or diagnostic tooling could obtain it. Reusing one fixed path also creates race and cross-invocation hazards. Reading `~/.config/gogcli/credentials.json` is relevant to the current authentication implementation because the script needs the OAuth client ID and clien ...[truncated 2001 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Prefer a supported `gog` API, standard Google credential provider, operating-system keychain, or secure token broker that returns credentials without exporting a refresh token to a filesystem path. 2. If a temporary file is unavoidable, create a unique file in a private directory using `tempfile.NamedTemporaryFile`, `tempfile.mkstemp`, or an equivalent secure primitive. 3. Create the file with permissions limited to the current user, such as mode `0600`, and ensure its parent directory is not accessible to other users. 4. Do not use a fixed filename in a shared directory. 5. Validate that the temporary object is a regular file owned by the expected user and do not follow symbolic links. 6. Read the token immediately and remove the temporary file in a `finally` block so cleanup occurs on success and failure. 7. Avoid printing token contents or exception objects that could include credentials. 8. Revoke and reissue any tokens that may already have been exposed through retained temporary files. 9. Reduce the token's OAuth scopes to the minimum needed for the requested operation, limiting the impact of any future disclosure. A safer implementation pattern is: ```python import os import tempfile token_path = None try: fd, token_path = tempfile.mkstemp(prefix="gog-slides-", suffix=".json") os.close(fd) os.chmod(token_path, 0o600) result = subprocess.run( [ "gog", "auth", "tokens", "export", ACCOUNT, "--out", token_path, "--overwrite" ], capture_output=True, text=True, check=False, ) if result.returncode != 0: raise RuntimeError("Token export failed") with open(token_path, encoding="utf-8") as token_file: token_data = json.load(token_file) finally: if token_path is not None: try: os.remove(token_path) except FileNotFoundError: pass ``` This pattern reduces, but does not elimina ...[truncated 64 chars]
