The Trailing Slash That Bypassed Authentication
GET /api/v2/account/settings HTTP/2
→ 401 Unauthorized
GET /api/v2/account/settings/ HTTP/2
→ 200 OK
One character. A forward slash at the end of the URL. That was the difference between "access denied" and "here's everything."
The First Hint
It started with a routine check. A protected API endpoint that required authentication. The request came back 401, as expected. No token, no access.
But something felt off about the way the server handled different request variations. Most well-configured APIs will return the same status code regardless of minor URL variations — with or without a trailing slash, the answer should be the same.
So I tried it:
/api/v2/account/settings → 401
/api/v2/account/settings/ → 200
The trailing slash version returned the full response body. No authentication token. No session cookie. Just... the data.
That was interesting. But one endpoint could be a misconfiguration. A one-off. The real question was: how deep does this go?
How Deep It Goes
The platform ran a microservice architecture. Multiple backend services, each handling a different domain — accounts, transactions, notifications, administration. They all sat behind a shared API gateway that handled authentication.
The authentication layer worked by matching incoming request paths against a list of protected routes. If the path matched, the middleware checked for a valid token. If it didn't match, the request passed through.
The route definitions looked something like this:
/api/v2/account/settings → requires auth
/api/v2/account/profile → requires auth
/api/v2/transactions/list → requires auth
/api/v2/admin/users → requires auth + admin role
Exact matches. No wildcard. No glob pattern. No normalization.
And the application routers behind the gateway? They treated /path and /path/ as identical. The framework normalized the URL before routing, stripping the trailing slash and matching the handler either way.
The gap between these two behaviors was the vulnerability.
The auth middleware saw /api/v2/account/settings/ and thought: "That's not in my protected routes list. Let it through."
The application router saw /api/v2/account/settings/ and thought: "That's /api/v2/account/settings. Here's the response."
Thirty Endpoints. Ten Services.
Once the pattern was clear, testing it was methodical. Every protected endpoint, with a trailing slash appended.
The results were systematic:
/api/v2/account/settings/ → bypassed
/api/v2/account/profile/ → bypassed
/api/v2/transactions/history/ → bypassed
/api/v2/notifications/config/ → bypassed
/api/v2/admin/users/ → bypassed
Thirty-plus endpoints across ten microservices. The authentication bypass wasn't a bug in one service — it was an architectural flaw in the shared gateway layer that affected everything behind it.
And it got worse.
The Method Problem
HTTP methods added another dimension. The authentication middleware didn't just match on path — it matched on path + method combinations. GET /api/v2/transactions required auth. OPTIONS /api/v2/transactions didn't (CORS preflight).
But what about PATCH /api/v2/account/settings/?
The middleware had rules for GET and POST on most endpoints. But PATCH, PUT, DELETE, and OPTIONS were inconsistently covered. Some endpoints had method-specific rules. Others relied on a catch-all that only matched common methods.
Combining the trailing slash bypass with uncommon HTTP methods opened even more surface:
PATCH /api/v2/account/settings/ → bypassed auth, could modify data
DELETE /api/v2/notifications/1/ → bypassed auth, could delete records
Read access was bad. Write access was worse.
Why This Is Architectural
This wasn't a typo in a configuration file. It was a fundamental mismatch between how two components — the authentication gateway and the application routers — interpreted the same URL.
In microservice architectures, this pattern is common and dangerous:
-
The gateway normalizes differently from the services. The gateway sees the raw URL. The services see a framework-normalized URL. If these don't agree, security decisions made at the gateway don't apply to what the service actually processes.
-
Route definitions drift. As new endpoints are added, the authentication route list needs to be updated. In a fast-moving codebase with ten services, keeping this list synchronized is manual and error-prone. A missed entry means an unprotected endpoint.
-
Framework defaults vary. One service might use a framework that strips trailing slashes. Another might preserve them. The gateway can't assume how each downstream service will handle the path.
The Fix
The fix operates at multiple levels:
Normalize before authenticating. The authentication middleware should normalize the request path — strip trailing slashes, resolve dot-segments, lowercase the path — before matching it against protected routes. This eliminates the mismatch.
Use pattern matching, not exact matching. Instead of matching /api/v2/account/settings exactly, match /api/v2/account/settings* or use regex patterns that account for trailing slashes and path variations.
Default to deny. Instead of listing routes that require authentication and allowing everything else through, list routes that are public and authenticate everything else. This way, a new endpoint is protected by default until explicitly marked as public.
Enforce at the framework level. In addition to gateway authentication, each microservice should enforce its own authorization. Defense in depth means the gateway failing doesn't mean the service is exposed.
The Takeaway
A URL is not as simple as it looks. /path and /path/ might be identical to a human, but they can be different strings to a computer. When security decisions depend on string matching, these small differences become vulnerabilities.
In distributed architectures, every component that touches a request path — the load balancer, the gateway, the middleware, the router, the handler — needs to agree on how that path is interpreted. One disagreement, and authentication becomes optional.
The trailing slash is one of the oldest gotchas in web development. It's been causing redirect loops and broken links since the early internet. But in a microservice architecture with shared authentication, it doesn't just break a link.
It breaks the lock on the door.