CVE-2025-27408
Description
Manifest offers users a one-file micro back end. Prior to version 4.9.2, Manifest employs a weak password hashing implementation that uses SHA3 without a salt. This exposes user passwords to a higher risk of being cracked if an attacker gains access to the database. Without the use of a salt, identical passwords across multiple users will result in the same hash, making it easier for attackers to identify and exploit patterns, thereby accelerating the cracking process. Version 4.9.2 fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
manifestnpm | < 4.9.2 | 4.9.2 |
Patches
2a528afbd35073ed6f1324e96Merge commit from fork
11 files changed · +365 −86
.changeset/famous-cougars-design.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'manifest': patch +--- + +replaced SHA3 encryption by bcrypt with salt, thanks @prokofitch @BennySama94
package-lock.json+197 −23 modified@@ -8123,6 +8123,164 @@ "node": ">= 4.0.0" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.0", "license": "MIT" @@ -10805,6 +10963,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "dev": true, @@ -11921,8 +12089,7 @@ }, "node_modules/aproba": { "version": "2.0.0", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/archiver": { "version": "7.0.1", @@ -12525,6 +12692,20 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -12534,6 +12715,12 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -13402,7 +13589,6 @@ "node_modules/color-support": { "version": "1.1.3", "license": "ISC", - "optional": true, "bin": { "color-support": "bin.js" } @@ -13680,8 +13866,7 @@ }, "node_modules/console-control-strings": { "version": "1.1.0", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/constant-case": { "version": "3.0.4", @@ -14050,10 +14235,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "license": "MIT" - }, "node_modules/css-loader": { "version": "6.10.0", "dev": true, @@ -14297,8 +14478,7 @@ }, "node_modules/delegates": { "version": "1.0.0", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/depd": { "version": "2.0.0", @@ -16324,8 +16504,7 @@ }, "node_modules/has-unicode": { "version": "2.0.1", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/hasown": { "version": "2.0.2", @@ -24972,7 +25151,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "devOptional": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -24986,7 +25164,6 @@ }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.11", - "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -24995,7 +25172,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -25014,7 +25190,6 @@ }, "node_modules/rimraf/node_modules/minimatch": { "version": "3.1.2", - "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -25398,8 +25573,7 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -28827,7 +29001,6 @@ "node_modules/wide-align": { "version": "1.1.5", "license": "ISC", - "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -29090,7 +29263,7 @@ "license": "MIT" }, "packages/add-manifest": { - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@oclif/core": "^3", @@ -29164,7 +29337,7 @@ } }, "packages/core/manifest": { - "version": "4.7.1", + "version": "4.9.1", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.744.0", @@ -29176,12 +29349,12 @@ "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "ajv": "^8.17.1", + "bcrypt": "^5.1.1", "chalk": "^4.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cli-table": "^0.3.11", "connect-livereload": "^0.6.1", - "crypto-js": "^4.2.0", "dasherize": "^2.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", @@ -29206,6 +29379,7 @@ "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.8", "@testcontainers/postgresql": "^10.18.0", + "@types/bcrypt": "^5.0.2", "@types/dasherize": "^2.0.3", "@types/express": "^4.17.21", "@types/js-yaml": "^4.0.9",
packages/core/admin/src/app/modules/auth/views/login/login.component.html+8 −8 modified@@ -38,15 +38,15 @@ <h2 class="title is-4">Sign in</h2> ></app-input> </div> </div> - </div> - <button - class="button is-block is-dark is-fullwidth mb-3" - (click)="submit()" - [disabled]="!form.valid" - > - Login - </button> + <button + class="button is-block is-dark is-fullwidth mb-3" + (click)="submit()" + [disabled]="!form.valid" + > + Login + </button> + </div> </div> </div> </div>
packages/core/admin/src/app/modules/auth/views/login/login.component.ts+3 −5 modified@@ -5,7 +5,6 @@ import { ActivatedRoute, Params, Router } from '@angular/router' import { PropType } from '@repo/types' -import { FlashMessageService } from '../../../shared/services/flash-message.service' import { AuthService } from '../../auth.service' import { DEFAULT_ADMIN_CREDENTIALS } from '../../../../../constants' @@ -25,8 +24,7 @@ export class LoginComponent implements OnInit { constructor( private readonly authService: AuthService, private router: Router, - private activatedRoute: ActivatedRoute, - private flashMessageService: FlashMessageService + private activatedRoute: ActivatedRoute ) {} ngOnInit(): void { @@ -64,11 +62,11 @@ export class LoginComponent implements OnInit { submit() { this.authService.login(this.form.value).then( - (res) => { + () => { this.router.navigate(['/']) }, (err: HttpErrorResponse) => { - this.flashMessageService.error( + alert( err.status === 401 ? `Error: Incorrect username or password.` : err.error.message
packages/core/manifest/package.json+2 −1 modified@@ -68,12 +68,12 @@ "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "ajv": "^8.17.1", + "bcrypt": "^5.1.1", "chalk": "^4.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cli-table": "^0.3.11", "connect-livereload": "^0.6.1", - "crypto-js": "^4.2.0", "dasherize": "^2.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", @@ -98,6 +98,7 @@ "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.8", "@testcontainers/postgresql": "^10.18.0", + "@types/bcrypt": "^5.0.2", "@types/dasherize": "^2.0.3", "@types/express": "^4.17.21", "@types/js-yaml": "^4.0.9",
packages/core/manifest/src/auth/auth.service.ts+53 −24 modified@@ -1,13 +1,17 @@ import { AuthenticableEntity, EntityManifest } from '@repo/types' import { HttpException, HttpStatus, Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' -import { SHA3 } from 'crypto-js' +import bcrypt from 'bcrypt' import { Request } from 'express' import * as jwt from 'jsonwebtoken' import { Repository } from 'typeorm' import { EntityService } from '../entity/services/entity.service' import { SignupAuthenticableEntityDto } from './dtos/signup-authenticable-entity.dto' -import { ADMIN_ENTITY_MANIFEST, DEFAULT_ADMIN_CREDENTIALS } from '../constants' +import { + ADMIN_ENTITY_MANIFEST, + DEFAULT_ADMIN_CREDENTIALS, + SALT_ROUNDS +} from '../constants' import { validate } from 'class-validator' import { EntityManifestService } from '../manifest/services/entity-manifest.service' @@ -46,17 +50,12 @@ export class AuthService { ) } - const entityRepository: Repository<AuthenticableEntity> = - this.entityService.getEntityRepository({ - entitySlug - }) as Repository<AuthenticableEntity> + const user = await this.findUserFromCredentials( + entitySlug, + signupUserDto.email, + signupUserDto.password + ) - const user = await entityRepository.findOne({ - where: { - email: signupUserDto.email, - password: SHA3(signupUserDto.password).toString() - } - }) if (!user) { throw new HttpException( 'Invalid email or password', @@ -107,11 +106,15 @@ export class AuthService { ) } - const entityRepository: Repository<any> = - this.entityService.getEntityRepository({ entitySlug }) + const entityRepository: Repository<AuthenticableEntity> = + this.entityService.getEntityRepository({ + entitySlug + }) as Repository<AuthenticableEntity> - const newUser: AuthenticableEntity = entityRepository.create(signupUserDto) - newUser.password = SHA3(newUser.password).toString() + const newUser: AuthenticableEntity = entityRepository.create({ + email: signupUserDto.email + }) + newUser.password = bcrypt.hashSync(signupUserDto.password, SALT_ROUNDS) const errors = await validate(newUser) if (errors.length) { @@ -204,18 +207,44 @@ export class AuthService { * @returns A promise that resolves to an object with the key 'exists' that is true if the default admin exists, and false otherwise. * */ async isDefaultAdminExists(): Promise<{ exists: boolean }> { + const admin: AuthenticableEntity = await this.findUserFromCredentials( + ADMIN_ENTITY_MANIFEST.slug, + DEFAULT_ADMIN_CREDENTIALS.email, + DEFAULT_ADMIN_CREDENTIALS.password + ) + + return { exists: !!admin } + } + + /** + * Find user from credentials. + * + * @param entitySlug The slug of the entity where the user is going to be searched + * @param email The email of the user + * @param password The password of the user + * + * @returns The user found from the credentials, or null if the user is not found. + */ + async findUserFromCredentials( + entitySlug: string, + email: string, + password: string + ): Promise<AuthenticableEntity> { const entityRepository: Repository<AuthenticableEntity> = this.entityService.getEntityRepository({ - entitySlug: ADMIN_ENTITY_MANIFEST.slug + entitySlug }) as Repository<AuthenticableEntity> - return { - exists: await entityRepository.exists({ - where: { - email: DEFAULT_ADMIN_CREDENTIALS.email, - password: SHA3(DEFAULT_ADMIN_CREDENTIALS.password).toString() - } - }) + const user: AuthenticableEntity = await entityRepository.findOne({ + where: { + email + } + }) + + if (!user || !bcrypt.compareSync(password, user.password)) { + return null } + + return user } }
packages/core/manifest/src/auth/tests/auth.service.spec.ts+51 −6 modified@@ -7,6 +7,11 @@ import { EntityService } from '../../entity/services/entity.service' import { ADMIN_ENTITY_MANIFEST } from '../../constants' import { EntityManifestService } from '../../manifest/services/entity-manifest.service' +jest.mock('bcrypt', () => ({ + compareSync: jest.fn().mockResolvedValue(true), + hashSync: jest.fn().mockResolvedValue('testHashedPassword') +})) + describe('AuthService', () => { let authService: AuthService let configService: ConfigService @@ -55,6 +60,10 @@ describe('AuthService', () => { describe('createToken', () => { it('should return a valid JWT token if a user is found', async () => { + jest + .spyOn(authService, 'findUserFromCredentials') + .mockResolvedValue(mockUser) + const result = await authService.createToken( ADMIN_ENTITY_MANIFEST.slug, mockUser @@ -108,6 +117,10 @@ describe('AuthService', () => { findOne: jest.fn().mockReturnValue(Promise.resolve(mockUser)) }) + jest + .spyOn(authService, 'findUserFromCredentials') + .mockResolvedValue(mockUser) + const result = await authService.signup('users', mockUser) expect(result).toHaveProperty('token') @@ -116,6 +129,10 @@ describe('AuthService', () => { describe('getUserFromToken', () => { it('should return a user when the token decodes to a valid email', async () => { + jest + .spyOn(authService, 'findUserFromCredentials') + .mockResolvedValue(mockUser) + entityService.getEntityRepository = jest.fn().mockReturnValue({ findOne: jest.fn().mockReturnValue(Promise.resolve(mockUser)) }) @@ -209,21 +226,49 @@ describe('AuthService', () => { describe('isDefaultAdminExists', () => { it('should return true if the default admin exists', async () => { - entityService.getEntityRepository = jest.fn().mockReturnValue({ - exists: jest.fn().mockReturnValue(Promise.resolve(true)) - }) + jest + .spyOn(authService, 'findUserFromCredentials') + .mockResolvedValue(mockUser) const result = await authService.isDefaultAdminExists() expect(result.exists).toBe(true) }) it('should return false if the default admin does not exist', async () => { - entityService.getEntityRepository = jest.fn().mockReturnValue({ - exists: jest.fn().mockReturnValue(Promise.resolve(false)) - }) + jest.spyOn(authService, 'findUserFromCredentials').mockResolvedValue(null) const result = await authService.isDefaultAdminExists() expect(result.exists).toBe(false) }) }) + + describe('findUserFromCredentials', () => { + it('should return a user if the credentials are correct', async () => { + entityService.getEntityRepository = jest.fn().mockReturnValue({ + findOne: jest.fn().mockReturnValue(Promise.resolve(mockUser)) + }) + + const result = await authService.findUserFromCredentials( + ADMIN_ENTITY_MANIFEST.slug, + mockUser.email, + mockUser.password + ) + + expect(result).toMatchObject(mockUser) + }) + + it('should return null if the credentials are incorrect', async () => { + entityService.getEntityRepository = jest.fn().mockReturnValue({ + findOne: jest.fn().mockReturnValue(Promise.resolve(null)) + }) + + const result = await authService.findUserFromCredentials( + ADMIN_ENTITY_MANIFEST.slug, + mockUser.email, + 'incorrectPassword' + ) + + expect(result).toBe(null) + }) + }) })
packages/core/manifest/src/constants.ts+3 −0 modified@@ -17,6 +17,9 @@ export const ENDPOINTS_PATH: string = 'endpoints' export const DEFAULT_PORT: number = 1111 export const DEFAULT_RESULTS_PER_PAGE: number = 20 +// Security. +export const SALT_ROUNDS: number = 10 + // Seeder. export const DEFAULT_SEED_COUNT: number = 50 export const DEFAULT_MAX_MANY_TO_MANY_RELATIONS: number = 5
packages/core/manifest/src/crud/services/crud.service.ts+11 −4 modified@@ -1,4 +1,4 @@ -import { SHA3 } from 'crypto-js' +import bcrypt from 'bcrypt' import { HttpException, HttpStatus, @@ -28,7 +28,8 @@ import { import { RelationMetadata } from 'typeorm/metadata/RelationMetadata' import { DEFAULT_RESULTS_PER_PAGE, - QUERY_PARAMS_RESERVED_WORDS + QUERY_PARAMS_RESERVED_WORDS, + SALT_ROUNDS } from '../../constants' import { PaginationService } from './pagination.service' @@ -241,7 +242,10 @@ export class CrudService { }) if (entityManifest.authenticable && itemDto.password) { - newItem.password = SHA3(newItem.password).toString() + newItem.password = bcrypt.hashSync( + itemDto['password'] as string, + SALT_ROUNDS + ) } const errors: ValidationError[] = this.validationService.validate( @@ -334,7 +338,10 @@ export class CrudService { // Hash password if it exists. if (entityManifest.authenticable && itemDto.password) { - updatedItem.password = SHA3(updatedItem.password).toString() + updatedItem.password = bcrypt.hashSync( + itemDto['password'] as string, + SALT_ROUNDS + ) } else if (entityManifest.authenticable && !itemDto.password) { delete updatedItem.password }
packages/core/manifest/src/seed/services/seeder.service.ts+19 −6 modified@@ -7,7 +7,7 @@ import { PropertyManifest, RelationshipManifest } from '@repo/types' -import { SHA3 } from 'crypto-js' + import { Injectable } from '@nestjs/common' import { DataSource, EntityMetadata, QueryRunner, Repository } from 'typeorm' import { EntityService } from '../../entity/services/entity.service' @@ -16,6 +16,7 @@ import { RelationshipService } from '../../entity/services/relationship.service' import { faker } from '@faker-js/faker' import * as fs from 'fs' import * as path from 'path' +import bcrypt from 'bcrypt' import { ADMIN_ENTITY_MANIFEST, @@ -121,9 +122,15 @@ export class SeederService { // Prevent logging during tests. if (process.env.NODE_ENV !== 'test') { - console.log( - `✅ Seeding ${entityManifest.seedCount} ${entityManifest.seedCount > 1 ? entityManifest.namePlural : entityManifest.nameSingular}...` - ) + if (entityManifest.single) { + console.log( + `✅ Seeding ${entityManifest.seedCount || 'single'} ${entityManifest.nameSingular}...` + ) + } else { + console.log( + `✅ Seeding ${entityManifest.seedCount} ${entityManifest.seedCount > 1 ? entityManifest.namePlural : entityManifest.nameSingular}...` + ) + } } for (const _index of Array(entityManifest.seedCount).keys()) { @@ -240,7 +247,7 @@ export class SeederService { case PropType.Boolean: return faker.datatype.boolean() case PropType.Password: - return SHA3('manifest').toString() + return bcrypt.hashSync('manifest', 1) case PropType.Choice: return faker.helpers.arrayElement( propertyManifest.options.values as unknown[] @@ -296,10 +303,16 @@ export class SeederService { * @param repository The repository for the Admin entity. */ async seedAdmin(repository: Repository<BaseEntity>): Promise<void> { + if (process.env.NODE_ENV !== 'test') { + console.log( + `✅ Seeding default admin ${DEFAULT_ADMIN_CREDENTIALS.email} with password "${DEFAULT_ADMIN_CREDENTIALS.password} ...` + ) + } + const admin: AuthenticableEntity = repository.create() as AuthenticableEntity admin.email = DEFAULT_ADMIN_CREDENTIALS.email - admin.password = SHA3(DEFAULT_ADMIN_CREDENTIALS.password).toString() + admin.password = bcrypt.hashSync(DEFAULT_ADMIN_CREDENTIALS.password, 1) await repository.save(admin) }
packages/core/manifest/src/seed/tests/seeder.service.spec.ts+13 −9 modified@@ -1,6 +1,4 @@ -// TODO: Ensure that the storeFile and storeImage methods are only called once per property. import { Test, TestingModule } from '@nestjs/testing' -import { SHA3 } from 'crypto-js' import { SeederService } from '../services/seeder.service' import { EntityService } from '../../entity/services/entity.service' @@ -16,6 +14,11 @@ jest.mock('fs', () => ({ readFileSync: jest.fn().mockResolvedValue('mock file content') })) +jest.mock('bcrypt', () => ({ + hashSync: jest.fn().mockResolvedValue('hashedPassword') +})) + +// TODO: Ensure that the storeFile and storeImage methods are only called once per property. describe('SeederService', () => { let service: SeederService let storageService: StorageService @@ -225,13 +228,13 @@ describe('SeederService', () => { expect(typeof result).toBe('boolean') }) it('should seed the "manifest" password', async () => { - const result = await service.seedProperty( + const result = (await service.seedProperty( { type: PropType.Password } as any, {} as any - ) + )) as string expect(typeof result).toBe('string') - expect(result).toBe(SHA3('manifest').toString()) + expect(result).toBe('hashedPassword') }) it('should seed a choice', async () => { const result = await service.seedProperty( @@ -299,10 +302,11 @@ describe('SeederService', () => { await service.seedAdmin(dummyRepository) - expect(dummyRepository.save).toHaveBeenCalledWith({ - email: DEFAULT_ADMIN_CREDENTIALS.email, - password: SHA3(DEFAULT_ADMIN_CREDENTIALS.password).toString() - }) + expect(dummyRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + email: DEFAULT_ADMIN_CREDENTIALS.email + }) + ) }) }) })
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
4News mentions
0No linked articles in our index yet.