T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/outlook_auth.py:46
- Finding
- OAuth Tokens and Client Secret Are Stored Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/outlook_auth.py:46-68`, `scripts/outlook_auth.py:188-203` **Vulnerability Type**: Plaintext credential storage with process-default filesystem permissions **Risk Level**: High ### Vulnerable Code ```python def save_config(config): CONFIG_DIR.mkdir(parents=True, exist_ok=True) with open(CONFIG_FILE, "w") as f: json.dump(config, f, indent=2) def load_credentials(): if not CREDS_FILE.exists(): return None with open(CREDS_FILE, "r") as f: return json.load(f) def save_credentials(creds): CONFIG_DIR.mkdir(parents=True, exist_ok=True) with open(CREDS_FILE, "w") as f: json.dump(creds, f, indent=2) ``` The persisted data includes bearer tokens, refresh tokens, and an optional client secret: ```python creds = { "access_token": access_token, "refresh_token": refresh_token, "expires_at": time.time() + expires_in_token - 60, "token_type": token_resp.get("token_type", "Bearer") } save_credentials(creds) config = { "client_id": client_id, "tenant_id": tenant_id, "client_secret": client_secret } save_config(config) ``` ### Technical Analysis The Skill writes `~/.outlook-microsoft/credentials.json` and `~/.outlook-microsoft/config.json` using ordinary `open(..., "w")` calls. It does not explicitly create the configuration directory with mode `0700` or the credential files with mode `0600`. Consequently, the resulting permissions depend on the user's current umask and any pre-existing directory or file permissions. In an environment with a permissive umask, shared home directory, container volume, backup process, or multi-user host, another local principal may be able to read the stored credentials. The data is stored as unencrypted JSON and includes: - A Microsoft Graph access token. - An `offline_access` refresh token. - An optional application client secret. - Tenant and client identifiers. Persisting the client se ...[truncated 1830 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Create the credential directory with owner-only permissions: ```python CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(CONFIG_DIR, 0o700) ``` 2. Create or replace credential files atomically with mode `0600`. For example, use `os.open` with explicit permissions and write through a temporary owner-only file before an atomic rename: ```python fd = os.open( CREDS_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600, ) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(creds, f, indent=2) os.chmod(CREDS_FILE, 0o600) ``` 3. Apply the same protection to `config.json`, and validate existing permissions before reading either file. Refuse insecure files or repair their permissions with an explicit warning. 4. Do not request, accept, or persist `OUTLOOK_CLIENT_SECRET` when operating as a public client with the device-code flow. Remove it from `.env`, setup instructions, configuration persistence, and refresh requests unless a separate confidential-client flow is intentionally implemented. 5. Prefer an operating-system credential store or secret-management service for refresh tokens rather than plaintext JSON. 6. Add clear token-revocation and logout functionality. Deleting the local credential file alone does not revoke a refresh token already copied by an attacker. 7. Avoid including token responses in diagnostic output, logs, exceptions, or backups. ]]>
