VYPR
Medium severity4.8NVD Advisory· Published Feb 28, 2025· Updated Apr 15, 2026

CVE-2025-27408

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.

PackageAffected versionsPatched versions
manifestnpm
< 4.9.24.9.2

Patches

2
3ed6f1324e96

Merge commit from fork

https://github.com/mnfst/manifestSébastien ConejoFeb 28, 2025via ghsa
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

4

News mentions

0

No linked articles in our index yet.