T09 · Insecure Skill Coding Practices
Error
- Location
- src/config.js:128
- Finding
- OAuth Tokens Are Stored Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/config.js:128-145` **Vulnerability Type**: Insecure storage of sensitive OAuth credentials **Risk Level**: High ### Vulnerable Code ```js function saveTokens(tokens, account = 'default') { if (!tokens || typeof tokens !== 'object') { throw new Error('saveTokens verwacht een tokens-object.'); } const { tokens: tokenPath } = getPaths(account); const toSave = { ...tokens }; // Houd refresh_token vast als provider hem niet altijd terugstuurt, // deze merge gebeurt in auth.js bij refresh. if (!toSave.expires_at) { const expiresIn = Number(toSave.expires_in || 0); if (Number.isFinite(expiresIn) && expiresIn > 0) { toSave.expires_at = Date.now() + (expiresIn * 1000); } } fs.writeFileSync(tokenPath, JSON.stringify(toSave, null, 2)); } ``` ### Technical Analysis The Skill persists Microsoft OAuth access and refresh tokens using `fs.writeFileSync` without specifying a restrictive file mode. The effective permissions therefore depend on the process umask. On systems with a common `0022` umask, a newly created token file may have mode `0644`, making it readable by other local users. The stored refresh token is a long-lived reusable credential. An attacker who reads it can submit it to Microsoft's OAuth token endpoint and obtain new access tokens without knowing the user's password. The resulting privileges include every delegated scope granted to the application. The implementation also does not verify whether the destination file is a symbolic link, whether it is owned by the current user, or whether an existing file has secure permissions before reading or overwriting it. ### Attack Path 1. A victim authenticates through the device-code flow. 2. The Skill writes the token response to `~/.openclaw/credentials/ms365.tokens.<account>.json`. 3. The token file inherits permissions from the process umask and may be readable by another local account or compromise ...[truncated 938 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Create the credential directory with owner-only permissions: ```js fs.mkdirSync(credentialsDir, { recursive: true, mode: 0o700 }); ``` 2. Write token files with mode `0600`: ```js fs.writeFileSync(tokenPath, JSON.stringify(toSave, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'w' }); fs.chmodSync(tokenPath, 0o600); ``` 3. Use atomic replacement: - Create a temporary file in the credential directory with `0600`. - Flush and close it. - Rename it over the destination. - Never use a globally writable temporary directory. 4. Before reading or replacing an existing token file: - Use `lstat` to reject symbolic links. - Verify that the file is owned by the current user. - Reject or repair group/world-readable permissions. - Consider opening files with no-follow semantics where supported. 5. Prefer an operating-system credential vault or keychain instead of plaintext JSON when available. 6. Document token revocation procedures and instruct users to revoke the application grant if token disclosure is suspected. ]]>
