Engineering Journal
Ginexys
Ginexys

Two Auth Bugs With the Same Root Cause: The Secondary Path Was Never Updated

2026-06-03

TLDR: A sensitive credential showed up in server access logs as a URL query parameter. A user sign-out left the server-side session actlive for up to an hour. Both issues were in secondary code paths—, a backup proxy and a sign-out function—, that were never updated when the main versions were improvprimary implementations were hardened.


Bug 1: Sensitive Credential in URL Query Parameter

Symptom

A polling endpoint was being called every 5 seconds during a sign-in handshake. The request URL contained ?secret=Y. That URL was written to server access logs on every poll request.

Why It Happens

URL query parameters are part of the request path. They show up in server access logs, CDN logs, load balancer logs, and browser history. Any credential in a URL query parameter ends up in at least four places before an attacker even tries anything.

The right way to send credentials is with the Authorization: Bearer <token> request header. Headers are not included in standard access logs.

The Fix

// wrong: credential in URL
const secret = url.searchParams.get('secret');
target.searchParams.set('secret', secret);

// correct: credential in header const secret = request.headers.get('Authorization')?.replace(/^Bearer\s+/i, '').trim(); return fetch(target.toString(), { headers: { Authorization: Bearer ${secret} }, });

The main proxy for this endpoint was already set up correctly. The backup proxy, though, was copied from an older version before the header-based method was used. It relied on the query parameter approach everywhere.

Guard

Before deploying any proxy or forwarding function that handles a secret or token, search the code for searchParams.set, ?secret=, or ?token=. If a credential shows up in the URL anywhere in the request chain, it will end up in the logs.

Lesson

Secondary endpoints must go through the same security review as primary ones. Fixing the main path does not mean every path is fixed.


Bug 2: Logout Did Not Clear the Server-Side Session Cookie

Symptom

After clicking Sign Out, the UI showed you were signed out and localStorage was empty. But if you sent a request to the authenticated API endpoint with the old session cookie, you still got a successful response for up to an hour.

Why It Happens

An HttpOnly cookie is hidden from JavaScript on purpose, and the browser makes sure of this. That’s the security feature. As a result, you can’t read, write, or delete an HttpOnly cookie from JavaScript. If you only clear localStorage on the client side, the HttpOnly cookie stays there.

The only way to expire an HttpOnly cookie is to have the server set Max-Age=0 on it. This means you need to make an HTTP request to the endpoint that manages the cookie.

The sign-out function cleared all Supabase localStorage keys and called the identity provider's sign-out. It felt complete because the client-side state was empty. But the server-side session was still live.

The Fix

function signOut() {
  // Clear the server-side cookie first. This is not optional.
  fetch('/api/auth/session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ action: 'clear' }),
  }).catch(() => {});

// Then clear client-side state identityProvider.auth.signOut().catch(() => {}); clearLocalStorageTokens(); resetAuthState(); }

The server replies with Set-Cookie: gx_session=; Max-Age=0, which tells the browser to expire the cookie right away.

Guard

Every logout path in any authentication flow must include a server round-trip to clear the HttpOnly cookie. To test: sign in, open browser DevTools, go to Application and then Cookies, click Sign Out, and check that the session cookie is gone.

Lesson

Only the server can clear HttpOnly cookies, not the client. Logout is not finished until Max-Age=0 is set. If your sign-out function only clears localStorage and the identity provider client, your sessions are still active.

Read this post in the full Engineering Journal →