From a435e011f8a2c98f52b67c4b96c3a7931a811541 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Mon, 27 Nov 2023 14:41:09 -0500 Subject: [PATCH] Security fix: update password system for database seeding --- README.md | 6 +++++- package-lock.json | 13 ++++++++++++ package.json | 9 ++++---- src/config/env/index.ts | 13 ++++++++++++ src/prisma/seed/create/createAdminUser.ts | 4 +++- src/prisma/seed/create/createNewUsers.ts | 26 ++++++++++++++++++++--- src/prisma/seed/index.ts | 2 +- 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0e1a6d0..3afa8c4 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,10 @@ POSTGRES_PRISMA_URL= POSTGRES_URL_NON_POOLING= SHADOW_DATABASE_URL= +ADMIN_PASSWORD= + MAPBOX_ACCESS_TOKEN= -NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN= + SPARKPOST_API_KEY= SPARKPOST_SENDER_ADDRESS=" > .env ``` @@ -175,6 +177,8 @@ SPARKPOST_SENDER_ADDRESS=" > .env - `SPARKPOST_API_KEY` is the API key for your SparkPost account. - You can create a free account [here](https://www.sparkpost.com/). - `SPARKPOST_SENDER_ADDRESS` is the email address that will be used to send emails. +- `ADMIN_PASSWORD` is the password for the admin account created when seeding the + database. 1. Initialize the database and run the migrations. diff --git a/package-lock.json b/package-lock.json index 13f0e15..b819fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "eslint-config-next": "^13.5.4", "eslint-config-prettier": "^9.0.0", "eslint-plugin-react": "^7.33.2", + "generate-password": "^1.7.1", "onchange": "^7.1.0", "postcss": "^8.4.26", "prettier": "^3.0.0", @@ -5354,6 +5355,12 @@ "node": ">=10" } }, + "node_modules/generate-password": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.1.tgz", + "integrity": "sha512-9bVYY+16m7W7GczRBDqXE+VVuCX+bWNrfYKC/2p2JkZukFb2sKxT6E3zZ3mJGz7GMe5iRK0A/WawSL3jQfJuNQ==", + "dev": true + }, "node_modules/geojson-vt": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", @@ -14595,6 +14602,12 @@ "wide-align": "^1.1.2" } }, + "generate-password": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.1.tgz", + "integrity": "sha512-9bVYY+16m7W7GczRBDqXE+VVuCX+bWNrfYKC/2p2JkZukFb2sKxT6E3zZ3mJGz7GMe5iRK0A/WawSL3jQfJuNQ==", + "dev": true + }, "geojson-vt": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", diff --git a/package.json b/package.json index 78ce408..e6f4f03 100644 --- a/package.json +++ b/package.json @@ -64,27 +64,28 @@ "@types/multer": "^1.4.7", "@types/node": "^20.4.2", "@types/passport-local": "^1.0.35", - "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/react": "^18.2.15", "@types/sparkpost": "^2.1.5", "@vercel/fetch": "^7.0.0", "autoprefixer": "^10.4.14", "daisyui": "^3.9.2", "dotenv-cli": "^7.2.1", - "eslint": "^8.51.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "17.1.0", "eslint-config-next": "^13.5.4", "eslint-config-prettier": "^9.0.0", "eslint-plugin-react": "^7.33.2", + "eslint": "^8.51.0", + "generate-password": "^1.7.1", "onchange": "^7.1.0", "postcss": "^8.4.26", - "prettier": "^3.0.0", "prettier-plugin-jsdoc": "^1.0.2", "prettier-plugin-tailwindcss": "^0.4.1", + "prettier": "^3.0.0", "prisma": "^5.6.0", - "tailwindcss": "^3.3.3", "tailwindcss-animate": "^1.0.6", + "tailwindcss": "^3.3.3", "ts-node": "^10.9.1", "typescript": "^5.3.2" }, diff --git a/src/config/env/index.ts b/src/config/env/index.ts index ecd1a7c..426e828 100644 --- a/src/config/env/index.ts +++ b/src/config/env/index.ts @@ -28,6 +28,8 @@ const envSchema = z.object({ SPARKPOST_API_KEY: z.string(), SPARKPOST_SENDER_ADDRESS: z.string().email(), MAPBOX_ACCESS_TOKEN: z.string(), + + ADMIN_PASSWORD: z.string().regex(/^(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9]).{8,}$/), }); const parsed = envSchema.safeParse(env); @@ -194,3 +196,14 @@ export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS; * @see https://docs.mapbox.com/help/how-mapbox-works/access-tokens/ */ export const MAPBOX_ACCESS_TOKEN = parsed.data.MAPBOX_ACCESS_TOKEN; + +/** + * Admin password for seeding the database. + * + * @example + * 'abcdefghijklmnopqrstuvwxyz123456'; + * + * @see README.md for instructions on generating a secret key. + */ + +export const ADMIN_PASSWORD = parsed.data.ADMIN_PASSWORD; diff --git a/src/prisma/seed/create/createAdminUser.ts b/src/prisma/seed/create/createAdminUser.ts index eeb3739..07b3d96 100644 --- a/src/prisma/seed/create/createAdminUser.ts +++ b/src/prisma/seed/create/createAdminUser.ts @@ -1,12 +1,14 @@ import { z } from 'zod'; import { hashPassword } from '../../../config/auth/passwordFns'; +import { ADMIN_PASSWORD } from '../../../config/env'; + import DBClient from '../../DBClient'; import GetUserSchema from '../../../services/User/schema/GetUserSchema'; import imageUrls from '../util/imageUrls'; const createAdminUser = async () => { - const hash = await hashPassword('Pas!3word'); + const hash = await hashPassword(ADMIN_PASSWORD); const adminUser: z.infer = await DBClient.instance.user.create({ data: { username: 'admin', diff --git a/src/prisma/seed/create/createNewUsers.ts b/src/prisma/seed/create/createNewUsers.ts index 96f6a2f..87878cd 100644 --- a/src/prisma/seed/create/createNewUsers.ts +++ b/src/prisma/seed/create/createNewUsers.ts @@ -1,8 +1,11 @@ -// eslint-disable-next-line import/no-extraneous-dependencies +/* eslint-disable import/no-extraneous-dependencies */ import { faker } from '@faker-js/faker'; +import generator from 'generate-password'; + import crypto from 'crypto'; import DBClient from '../../DBClient'; import { hashPassword } from '../../../config/auth/passwordFns'; +import logger from '../../../config/pino/logger'; interface CreateNewUsersArgs { numberOfUsers: number; @@ -23,9 +26,25 @@ interface UserData { const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { const prisma = DBClient.instance; + await DBClient.instance.$disconnect(); + + const passwords = Array.from({ length: numberOfUsers }, () => + generator.generate({ + length: 20, + symbols: true, + numbers: true, + uppercase: true, + strict: true, + }), + ); + + logger.info('Hashing passwords. This may take a while...'); + const hashedPasswords = await Promise.all( + passwords.map((password) => hashPassword(password)), + ); + + logger.info('Creating new users. This may take a while...'); - const password = 'passwoRd!3'; - const hash = await hashPassword(password); const data: UserData[] = []; const takenUsernames: string[] = []; @@ -41,6 +60,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => { .email({ firstName, lastName, provider: 'example.com' }) .toLowerCase(); + const hash = hashedPasswords[i]; const userAvailable = !takenUsernames.includes(username) && !takenEmails.includes(email); diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 2b44cbc..059d2c6 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -32,7 +32,7 @@ import createNewUserFollows from './create/createNewUserFollows'; await createAdminUser(); logger.info('Admin user created successfully.'); - const users = await createNewUsers({ numberOfUsers: 10000 }); + const users = await createNewUsers({ numberOfUsers: 1000 }); logger.info('Users created successfully.'); const userAvatars = await createNewUserAvatars({ joinData: { users } });