Skip to content

Five Security Holes, Locked Instead of Fixed

Adityo Guni Waluyo

A tests-only commit turns five weak authentication behaviors into a named inventory with owner-decision markers.

TL;DR

Instead of silently fixing five weak security behaviors, the author locked them as characterization tests with owner-decision markers. Findings include account enumeration via registration, unthrottled OTP verification, non-revoked JWTs, orphaned OTP rows, and in-memory rate-limiter state. Mapping first beats fixing from memory: each weakness becomes a prioritized decision ticket.

I opened the new test files for an authentication module and ran straight into five identical comment lines: // characterization: current behavior, owner decision pending. Five weak security behaviors, neither fixed nor silenced. They were locked as-is as characterization tests, each carrying a marker saying the decision belongs to the project owner. From the outside this looks like postponing work that should be done now. After reading all five, I concluded the opposite: this is the most honest way to map a risk surface before changing anything.

Five Findings, Locked

First, registration leaks account existence. The register endpoint answers with a specific 409 when an email already exists and is verified. A response that explicit is comfortable for users, but it doubles as an enumeration oracle: an outsider can map which addresses are registered just by watching the differences. OWASP states plainly that badly implemented authentication error messages "can be used for the purposes of user ID and password enumeration", and that an application should respond generically [1]. The same codebase already applies the anti-enumeration pattern on its OTP-resend endpoint with generic responses. Two philosophies coexist, and the commit takes no side; it only locks the facts.

Second, OTP verification runs outside the rate limiter. Login is tightly capped, but the HTTP path for guessing OTP codes passes no limiter that wraps the other credential endpoints. An attempt cap exists at the service layer, yet as long as the request path is unmetered, the cost of an attack is computed from the loosest side.

Third, changing the password does not revoke outstanding JWTs. OWASP writes that after authentication, a session ID or token "is temporarily equivalent to the strongest authentication method used by the application" [2]. An old token left unrevoked carries the strength of the credential that was just replaced. If it leaks, session hijacking remains possible exactly as if no credential change had happened.

Fourth, the OTP row is stored before the email is sent. The issueOTP function writes the database row first, then calls the mail sender. When the send fails, the row stays. That preserves the audit trail, but it leaves state desynchronized from reality, and this kind of state usually bites when another process reads it later.

Fifth, the rate limiter counters live in process memory. The Twelve-Factor methodology requires processes to be "stateless and share-nothing" and warns that "chances are high that a future request will be served by a different process" [3]. The consequences are concrete: a restart zeroes the counters, and two replicas mean two separate attempt budgets that never share.

Why Locked, Not Fixed

My first instinct was, of course, to fix them immediately. But fixing silently erases history: no record of why the behavior was ever chosen, no baseline for the next person proposing a change. Characterization tests turn five weaknesses into a named inventory. Each test carries the behavior's name, assertions that freeze the facts, and one decision marker. The marker is not an excuse to postpone forever; it is an explicit decision ticket that can be prioritized and called due.

What I appreciate in this pattern: it does not pretend all findings share one urgency. Five markers mean five separate discussions, each with its own trade-offs. The 409 response can survive for user experience and transition gradually to the generic pattern. The process-local limiter rises in priority because it touches distribution. Token revocation can be negotiated with a shorter expiry window as the middle path. Map first, then walk.

Three Questions for Judging Similar Findings

First, does the behavior violate a foundational principle, or is it merely impolite? A token that stays valid after a credential change touches the definition of security itself; that class rarely tolerates long delays. Second, is the codebase already consistent? When one endpoint already implements the anti-enumeration pattern, the one that does not becomes a standardization candidate, not a from-scratch design debate. Third, does the logic depend on process-local state? Security components storing data in local memory fall apart in distributed deployments; that is a strong signal to move to shared storage.

Once these five findings sit in the inventory, the owner's decision is about order and cost, not about rediscovering the problem. That is the difference between fixing from memory and fixing from a map.

Sources

[1] OWASP Authentication Cheat Sheet, accessed 11 October 2026.
[2] OWASP Session Management Cheat Sheet, accessed 11 October 2026.
[3] The Twelve-Factor App: Processes, accessed 11 October 2026.

Related articles