T09 · Insecure Skill Coding Practices
Warning
- Location
- SKILL.md:155
- Finding
- Plaintext Storage of Replayable Refresh Tokens## Vulnerability Details **File Location**: `SKILL.md`, lines 155–163 and 271–278 **Vulnerability Type**: Plaintext storage of sensitive bearer tokens **Risk Level**: Medium The Skill recommends storing complete refresh tokens directly in a database and subsequently locating them through plaintext token equality: ```javascript // Store refresh token in database await db.refreshToken.create({ data: { token: refreshToken, userId: user.id, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) } }); ``` ```javascript // Check if refresh token exists in database const storedToken = await db.refreshToken.findFirst({ where: { token: refreshToken, userId: decoded.userId, expiresAt: { gt: new Date() } } }); ``` ### Technical Analysis Refresh tokens are bearer credentials: possession is sufficient to exchange them for access tokens. Storing the original token makes every active database record immediately usable if the refresh-token table, a database backup, a replication endpoint, an administrative interface, or an accidentally generated database log is exposed. Unlike password hashes, the stored values require no cracking. The attacker can submit a copied value directly to the documented `/api/auth/refresh` endpoint. The example also does not rotate refresh tokens after successful use or detect reuse of an invalidated token, allowing a stolen token to be replayed repeatedly until it expires or is manually revoked. The use of signed JWT refresh tokens does not mitigate this issue. A valid stolen JWT remains a usable bearer credential even if the attacker does not know its signing secret. ### Attack Path 1. An attacker obtains read access to the refresh-token table or a copy of it through a database disclosure, exposed backup, excessive internal privileges, or another data-access vulnerability. 2. The attacker copies an unexpired plaintext value from the `token` fi ...[truncated 977 chars]
- Remediation
- ## Remediation Suggestions 1. Replace JWT refresh tokens with opaque, cryptographically random tokens generated using a secure random-number generator. 2. Return the original opaque token only to the client and store a keyed hash, such as HMAC-SHA-256 with a server-held key, in the database. 3. Hash the token presented to the refresh endpoint and compare that derived value with the stored digest. Use constant-time comparison where application-level comparisons are performed. 4. Rotate the refresh token after every successful refresh. Atomically invalidate the previous token before issuing and storing the replacement. 5. Organize tokens into session or token families. If an invalidated token is reused, revoke the entire family and require the user to authenticate again. 6. Preserve expiration, revocation, user, device, and creation metadata, but never log or persist the raw bearer token outside the client response. 7. Protect the hashing or HMAC key through a secret manager and rotate it under a documented key-rotation procedure. 8. Restrict database and backup access according to least privilege, encrypt backups, and monitor unusual reads of the token table. 9. For browser clients, deliver refresh tokens through `Secure`, `HttpOnly`, and appropriately configured `SameSite` cookies, with CSRF protections where required.
