google / google/webauthndemo

Silent fallback to a public express-session secret

Open
#140 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
634
Forks
132
PR merge metrics
No merged PRs in 30d

Description

# Silent fallback to a public express-session secret

## Summary

WebAuthnDemo silently falls back to the publicly known value `secret` when the `SECRET` environment variable is missing:

[`src/libs/config.mts:74`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/libs/config.mts#L74)

```ts
return session({
name: session_name,
secret: process.env.SECRET || 'secret',
resave: false,
saveUninitialized: false,
store: new FirestoreStore({
dataset: store,
kind: 'express-sessions',
}),
});
```

The fallback is not rejected at startup. The production and development App Engine configuration files do not define `SECRET`:

- [`app.yaml:1`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/app.yaml#L1)
- [`dev.yaml:1`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/dev.yaml#L1)

The repository also contains the predictable development value `SECRET='this is a secret'`:

- [`src/.env.development:1`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/.env.development#L1)

## Security impact

`SECRET` signs the `express-session` cookie. In remote deployments the cookie is named `__Host-session`; in local development it is named `session`:

[`src/libs/config.mts:74`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/libs/config.mts#L74)

An attacker who knows the fallback can generate a valid signature for a known session ID. This can enable replay of a server-side session when the attacker obtains the corresponding session ID without its valid signature, for example through session-ID disclosure or a separate session-fixation condition.

This finding does **not** prove a direct arbitrary-user login bypass by itself. The application uses `FirestoreStore`, so the session contents are stored server-side. The authorization middleware requires `req.session.user_id`:

- [`src/server.mts:37`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/server.mts#L37)
- [`src/libs/helper.mts:55`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/libs/helper.mts#L55)

A signed cookie for an ID with no matching Firestore session is therefore not sufficient to create an authenticated user. Login itself verifies a Firebase ID token before writing `user_id` and other identity data into the session:

[`src/libs/auth.mts:43`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/libs/auth.mts#L43)

The nearby random fallback at [`src/libs/config.mts:106`](https://github.com/google/webauthndemo/blob/f1cd94214ebafabe827ac888d3cca5327194933c/src/libs/config.mts#L106) does not mitigate this issue. `initializeSession()` reads `process.env.SECRET` independently and still selects the literal `secret`.

## Proof of concept

The following generates a valid `express-session` signature for an attacker-chosen session ID using the published fallback:

```sh
node <<'NODE'
const crypto = require('node:crypto');

const secret = 'secret';
const sessionId = 'known-session-id';
const signature = crypto
.createHmac('sha256', secret)
.update(sessionId)
.digest('base64')
.replace(/=+$/, '');

console.log(`s:${sessionId}.${signature}`);
NODE
```

Use the resulting value only against a local or authorized deployment:

```sh
curl -i -X POST \
-H 'Cookie: __Host-session=' \
https://target.example/auth/userInfo
```

If `known-session-id` has no corresponding Firestore record, the expected response is `401 Unauthorized`. If an attacker has obtained a valid Firestore session ID through an independent weakness, the same signed cookie can cause that server-side session to be accepted.

## Recommended fix

Treat a missing `SECRET` as a deployment error. Production and hosted development deployments should fail closed during startup unless `SECRET` is explicitly supplied as a deployment-specific, high-entropy value. Do not use `secret` or `this is a secret` as a fallback. If a generated value is used for local development, persist it securely; generating a new value on every restart invalidates existing sessions.

## Classification

- Root cause: public static fallback for the `express-session` signing secret.
- Authentication model: server-side session authentication backed by Firestore, not JWT authentication.
- Direct impact from the fallback alone: no demonstrated arbitrary-user authentication bypass.
- Conditional impact: session replay or fixation when combined with session-ID disclosure or another session-management weakness.

This report is part of my ongoing research on authentication mechanism security. Thank you for taking the time to review it. If you have any questions or would like additional validation, please feel free to @mention me or contact me at any time. I would be very glad to contribute, however modestly, to improving the security of WebAuthnDemo.

Contributor guide

Open the contributing guide

Research direction

Start with initializeSession() in src/libs/config.mts and compare its SECRET handling with app.yaml, dev.yaml, and src/.env.development. Review the related authorization and login paths in src/server.mts and src/libs/auth.mts, then verify that deployments without an explicit SECRET fail closed and that supplied secrets are used without predictable fallbacks.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.