Pentest Finding · 9hvwgp · CVSS 5.3 Medium
Improper Session Expiry — Token Reuse After Logout
prod1app.proctor360.com (application-wide)proctor-node (AdonisJS)pentest-fixes-2026Why it matters: the endpoint used (student update) is incidental — the same replay works against any authenticated route in proctor-node, because the JWT check that guards every route is the thing that's broken, not the student controller. Report scope says Affected Modules: application wide.
| Piece | File | Problem |
|---|---|---|
| Logout handler | AuthController.js:291-297 |
Calls auth.logout() — AdonisJS's JwtScheme never implements this method (it's in unimplementedMethods on the base scheme). It throws / no-ops. Nothing is revoked. |
| Token validation | JwtScheme.check() (vendor, @adonisjs/auth) |
Only verifies signature + expiry (expiresIn: 1440m = 24h). No revocation/blocklist lookup exists anywhere in the codebase. |
| Token shape | Minted in AuthController.js:58,86,112, SSO controllers |
Payload is just { uid } — no unique per-token id, so nothing to revoke individually even if we wanted to. |
| Choke point | @adonisjs/auth Middleware/Auth.js |
Good news: every .middleware(['auth']) route in start/routes.js funnels through this one place. One fix here covers the whole app. |
② is new infrastructure not shown as a box above: a Redis client + TokenRevocationService.js backing the revoked:jti:* lookups.
Only the token used to call logout is revoked — other devices/tabs for the same user keep working.
| # | Change | Files | |
|---|---|---|---|
| ① | Every newly-minted token gets a unique jti claim added to its payload. |
JWT token generation (AuthController.js login/register, SSO controllers) |
EDIT |
| ② | New Redis client + a small service to read/write revocation keys. | config/redis.js, app/Services/TokenRevocationService.js |
NEW |
| ③ | After the existing signature/expiry check passes, look up revoked:jti:<jti> in Redis; reject with the same "invalid token" error already used today if found. |
JwtScheme.check() hook |
EDIT |
| ④ | Replace the broken auth.logout() call with logic that writes the revocation key for only the current token, TTL'd to its remaining lifetime. |
AuthController.js:291-297 |
EDIT |
jti is internal-only — the response shape the frontend already expects ({ type, token, refreshToken }) is untouched.jti, not the user id — logging out on one device/tab does not kill sessions on other devices, same as today.tokens.is_revoked column — schema stays untouched.jti and simply can't be individually revoked — they behave exactly as they do today (valid until natural 24h expiry). Not worse than the current state, just not retroactively fixed.Backend: proctor-node only, for this specific finding (the PoC replay hit the student-update route, which still runs on legacy proctor-node — student CRUD hasn't migrated to proctor-nest yet).
Note: proctor-nest has the same class of bug on the auth flows it already owns (login/logout/refresh already route there per utils/session.js) — it currently only revokes the refresh token, not the short-lived access token. Recommended as a fast-follow using the same pattern once proctor-node ships.
Branch: pentest-fixes-2026 (needs rebase onto master first — currently 20 commits behind).