T09 · Insecure Skill Coding Practices
- Location
- scripts/utils.py:13
- Finding
- OAuth access and refresh tokens are stored without restrictive filesystem permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/utils.py:13-16` and `scripts/utils.py:40-42` **Vulnerability Type**: Insecure storage of OAuth credentials **Risk Level**: High ### Vulnerable Code ```python STATE_DIR = WORKSPACE_ROOT / "state" STATE_DIR.mkdir(exist_ok=True) AUTH_FILE = STATE_DIR / "graph_auth.json" LOG_FILE = STATE_DIR / "graph_ops.log" ``` ```python def save_auth_state(data: Dict[str, Any]) -> None: with AUTH_FILE.open("w", encoding="utf-8") as f: json.dump(data, f, indent=2) ``` ### Technical Analysis The authentication state contains Microsoft Graph access and refresh tokens. The code creates the state directory and authentication file using process-default permissions rather than explicitly enforcing owner-only access. Under a common `umask` of `022`, a newly created file can receive mode `0644`, making it readable by other local users. The containing directory may similarly be created with mode `0755`. A refresh token is particularly sensitive because it can be exchanged repeatedly for new access tokens until revoked. The implementation also writes directly to the final path instead of using a protected temporary file followed by an atomic replacement. It does not verify that the destination is owned by the expected user or reject symbolic links. ### Attack Path 1. A user completes the device-code login flow. 2. `save_auth_state()` writes the access token and refresh token to `state/graph_auth.json`. 3. The host has a permissive default `umask`, resulting in a file readable by another local account. 4. The local attacker reads the authentication file. 5. The attacker submits the refresh token to the Microsoft identity token endpoint using the recorded client and tenant identifiers. 6. The attacker receives a valid Graph access token and invokes APIs covered by the granted scopes. ### Impact Assessment A successful attacker can obtain the same delegated Microsoft Graph privileges as the authenticated user ...[truncated 491 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Create the state directory with mode `0700`. - Create the authentication file with mode `0600`, independent of the caller's `umask`. - Write credentials atomically through a same-directory temporary file opened with exclusive, owner-only permissions. - Validate that the state directory and credential file are owned by the current user. - Reject symbolic-link destinations and unexpected non-regular files. - Consider using the operating system credential store or a dedicated secrets manager. Example hardening approach: ```python STATE_DIR.mkdir(mode=0o700, parents=True, exist_ok=True) os.chmod(STATE_DIR, 0o700) def save_auth_state(data: Dict[str, Any]) -> None: temp_path = AUTH_FILE.with_suffix(".tmp") fd = os.open(temp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) try: with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) f.flush() os.fsync(f.fileno()) os.replace(temp_path, AUTH_FILE) os.chmod(AUTH_FILE, 0o600) finally: if temp_path.exists(): temp_path.unlink() ``` Existing installations should immediately change permissions on `state/` and `state/graph_auth.json` and rotate tokens if unauthorized local access may have occurred. ]]>
