VYPR
Medium severity5.6GHSA Advisory· Published Dec 2, 2025· Updated Apr 29, 2026

CVE-2025-13877

CVE-2025-13877

Description

A vulnerability was detected in nocobase up to 1.9.4/2.0.0-alpha.37. The affected element is an unknown function of the file nocobase\packages\core\auth\src\base\jwt-service.ts of the component JWT Service. The manipulation of the argument API_KEY results in use of hard-coded cryptographic key . The attack can be launched remotely. A high complexity level is associated with this attack. The exploitability is described as difficult. The exploit is now public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@nocobase/authnpm
>= 1.9.0, < 1.9.231.9.23
@nocobase/authnpm
< 1.9.0-beta.181.9.0-beta.18
@nocobase/authnpm
>= 2.0.0-alpha.1, < 2.0.0-alpha.522.0.0-alpha.52

Affected products

1

Patches

1
de4292ea7847

fix(auth): CVE-2025-13877 (#8128)

https://github.com/nocobase/nocobaseYANG QIADec 9, 2025via ghsa
5 files changed · +41 23
  • packages/core/auth/src/auth-manager.ts+29 1 modified
    @@ -13,6 +13,10 @@ import { Auth, AuthExtend } from './auth';
     import { JwtOptions, JwtService } from './base/jwt-service';
     import { ITokenBlacklistService } from './base/token-blacklist-service';
     import { ITokenControlService } from './base/token-control-service';
    +import path from 'path';
    +import fs from 'fs';
    +import crypto from 'crypto';
    +
     export interface Authenticator {
       authType: string;
       options: Record<string, any>;
    @@ -49,7 +53,11 @@ export class AuthManager {
     
       constructor(options: AuthManagerOptions) {
         this.options = options;
    -    this.jwt = new JwtService(options.jwt);
    +    const jwtOptions = options.jwt || ({} as JwtOptions);
    +    if (!jwtOptions.secret) {
    +      jwtOptions.secret = this.getDefaultAPIKey();
    +    }
    +    this.jwt = new JwtService(jwtOptions);
       }
     
       setStorer(storer: Storer) {
    @@ -142,4 +150,24 @@ export class AuthManager {
           await next();
         };
       }
    +
    +  private getDefaultAPIKey(): Buffer | string {
    +    const apiKeyPath = path.resolve(process.cwd(), 'storage', 'apps', 'main', 'api_key.dat');
    +    const appKeyExists = fs.existsSync(apiKeyPath);
    +    if (appKeyExists) {
    +      const key = fs.readFileSync(apiKeyPath);
    +      if (key.length !== 32) {
    +        throw new Error('Invalid api key length in file');
    +      }
    +      return key;
    +    }
    +    const envKey = process.env.APP_KEY;
    +    if (envKey && envKey !== 'your-secret-key' && envKey !== 'test-key') {
    +      return envKey;
    +    }
    +    const key = crypto.randomBytes(32);
    +    fs.mkdirSync(path.dirname(apiKeyPath), { recursive: true });
    +    fs.writeFileSync(apiKeyPath, key, { mode: 0o600 });
    +    return key;
    +  }
     }
    
  • packages/core/auth/src/base/jwt-service.ts+4 7 modified
    @@ -9,22 +9,19 @@
     
     import jwt, { JwtPayload, SignOptions } from 'jsonwebtoken';
     import { ITokenBlacklistService } from './token-blacklist-service';
    +
     export interface JwtOptions {
    -  secret: string;
    +  secret: Buffer | string;
       expiresIn?: string;
     }
     
     export type SignPayload = Parameters<typeof jwt.sign>[0];
     
     export class JwtService {
    -  constructor(
    -    protected options: JwtOptions = {
    -      secret: process.env.APP_KEY,
    -    },
    -  ) {
    +  constructor(protected options: JwtOptions) {
         const { secret, expiresIn } = options;
         this.options = {
    -      secret: secret || process.env.APP_KEY,
    +      secret,
           expiresIn: expiresIn || process.env.JWT_EXPIRES_IN || '7d',
         };
       }
    
  • packages/core/test/src/server/mock-server.ts+2 2 modified
    @@ -124,7 +124,7 @@ export class MockServer extends Application {
       agent(callback?): ExtendedAgent {
         const agent = supertest.agent(callback || this.callback());
         const prefix = this.resourcer.options.prefix;
    -    const authManager = this.authManager;
    +    const authManager = this.authManager as any;
         const proxy = new Proxy(agent, {
           get(target, method: string, receiver) {
             if (['login', 'loginUsingId'].includes(method)) {
    @@ -142,7 +142,7 @@ export class MockServer extends Application {
                         roleName,
                         signInTime: Date.now(),
                       },
    -                  process.env.APP_KEY,
    +                  authManager.jwt.secret(),
                       {
                         jwtid: tokenInfo.jti,
                         expiresIn,
    
  • packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts+2 3 modified
    @@ -10,7 +10,6 @@
     import Database, { BelongsToManyRepository } from '@nocobase/database';
     import UsersPlugin from '@nocobase/plugin-users';
     import { createMockServer, MockServer } from '@nocobase/test';
    -import jwt from 'jsonwebtoken';
     import { SystemRoleMode } from '../enum';
     import { UNION_ROLE_KEY } from '../constants';
     
    @@ -146,7 +145,7 @@ describe('role', () => {
         await userRolesRepo.add('test1');
         await userRolesRepo.add('test2');
     
    -    const userToken = jwt.sign({ userId: user.get('id') }, 'test-key');
    +    const userToken = api.authManager.jwt.sign({ userId: user.get('id') });
         const response = await api
           .agent()
           .post('/users:setDefaultRole')
    @@ -188,7 +187,7 @@ describe('role', () => {
             roleMode: SystemRoleMode.allowUseUnion,
           },
         });
    -    const userToken = jwt.sign({ userId: user.get('id') }, 'test-key');
    +    const userToken = api.authManager.jwt.sign({ userId: user.get('id') });
         const response = await api
           .agent()
           .post('/users:setDefaultRole')
    
  • packages/plugins/@nocobase/plugin-workflow-request/src/server/__tests__/instruction.test.ts+4 10 modified
    @@ -625,16 +625,10 @@ describe('workflow > instructions > request', () => {
         it('request db resource', async () => {
           const user = await db.getRepository('users').create({});
     
    -      const token = jwt.sign(
    -        {
    -          userId: user.id,
    -          signInTime: Date.now(),
    -        },
    -        process.env.APP_KEY,
    -        {
    -          expiresIn: '1d',
    -        },
    -      );
    +      const token = app.authManager.jwt.sign({
    +        userId: user.id,
    +        signInTime: Date.now(),
    +      });
     
           const server = app.listen(0, () => {});
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

14

News mentions

0

No linked articles in our index yet.