T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/api.js:36
- Finding
- Long-Lived Authentication Credentials Stored in Plaintext Files<![CDATA[ ## Vulnerability Details **File Location**: `scripts/api.js:36-44`, with related credential persistence at `scripts/api.js:149-165` and `scripts/refresh-token.js:75-94` **Vulnerability Type**: Plaintext storage of reusable authentication credentials **Risk Level**: Medium ### Vulnerable Code ```js function saveTokenCache(data) { fs.writeFileSync(CONFIG.tokenCacheFile, JSON.stringify({ token: data.token, tokenExpireAt: data.tokenExpireAt || null, refreshToken: data.refreshToken || null, refreshTokenExpireAt: data.refreshTokenExpireAt || null, savedAt: new Date().toISOString(), }, null, 2), 'utf8'); } ``` The browser authentication state and extracted credentials are also persisted: ```js const authInfo = await page.evaluate(() => ({ token: localStorage.getItem('token'), tokenExpireAt: localStorage.getItem('token_expire_at'), refreshToken: localStorage.getItem('refresh_token'), refreshTokenExpireAt: localStorage.getItem('refresh_token_expire_at'), })); token = authInfo.token; await context.storageState({ path: CONFIG.authStateFile }); saveTokenCache({ token: authInfo.token, tokenExpireAt: authInfo.tokenExpireAt ? parseInt(authInfo.tokenExpireAt) : null, refreshToken: authInfo.refreshToken || null, refreshTokenExpireAt: authInfo.refreshTokenExpireAt ? parseInt(authInfo.refreshTokenExpireAt) : null, }); ``` The separate refresh utility similarly persists authentication data without explicitly restricting file permissions: ```js const authInfo = await page.evaluate(() => ({ token: localStorage.getItem('token'), tokenExpireAt: localStorage.getItem('token_expire_at'), refreshToken: localStorage.getItem('refresh_token'), refreshTokenExpireAt: localStorage.getItem('refresh_token_expire_at') })); if (!authInfo.token) { console.error('❌ Failed to get token from page. May need manual re-login.'); process.exit(1); } // Save new token fs.writeFileSync(T ...[truncated 3260 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Store refresh tokens and session credentials in an operating-system credential manager, such as macOS Keychain, Windows Credential Manager, or a Linux Secret Service implementation. 2. If file-based storage is unavoidable, create credential files with owner-only permissions: ```js fs.writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o600, }); ``` 3. After writing Playwright storage state, explicitly set and verify its permissions: ```js await context.storageState({ path: CONFIG.authStateFile }); fs.chmodSync(CONFIG.authStateFile, 0o600); ``` 4. Before reading a credential file, use `lstat` and `stat` to reject symbolic links, unexpected owners, and group/world-readable permissions. 5. Avoid duplicating the refresh token across `.token-cache.json` and `.auth-state.json`. Retain only the minimum authentication material needed for synchronization. 6. Ship a `.gitignore` containing at least: ```gitignore .token-cache.json .auth-state.json .sync-state.json ``` 7. Add startup checks that warn or fail securely if sensitive files are tracked by Git or have unsafe permissions. 8. Document how users can revoke active sessions and refresh tokens after suspected exposure. 9. Consider encrypting any unavoidable on-disk credential cache with a key held by the operating-system credential manager. ]]>
