From 0c982239b7ac87b38efa124409c944532d643002 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:31:01 +0000 Subject: [PATCH 1/2] fix: remove hardcoded admin credentials Migrated hardcoded admin username and password from `admin-auth.ts` to environment variables validated via `env.ts` with Zod. Updated `.env.example` and CI workflow to include the new required environment variables. --- .github/workflows/ci.yml | 2 ++ .jules/sentinel.md | 4 ++++ packages/web/.env.example | 2 ++ packages/web/src/lib/server/admin-auth.ts | 4 ++-- packages/web/src/lib/server/env.ts | 2 ++ 5 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc24903..00e9359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,5 @@ jobs: JWT_SECRET: "ci-placeholder-jwt-secret-min-32-chars" DATABASE_URL: "postgresql://placeholder:placeholder@localhost:5432/placeholder" DIRECT_URL: "postgresql://placeholder:placeholder@localhost:5432/placeholder" + ADMIN_USERNAME: "admin" + ADMIN_PASSWORD: "ci-placeholder-admin-password" diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..477e86f --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-14 - Hardcoded Admin Credentials +**Vulnerability:** A critical security vulnerability was found in `packages/web/src/lib/server/admin-auth.ts` where the admin username (`admin`) and admin password (`og9oRajx7h88v1RIj3eDgdrh9jgLYVV3`) were hardcoded in the source code. This is dangerous because anyone with access to the source code repository can easily extract these secrets and compromise the admin interface. +**Learning:** Hardcoding credentials exposes sensitive data to unauthorized parties via source control systems. This practice circumvents the principle of least privilege. +**Prevention:** Always define secrets in environment configuration systems (e.g. `.env`) and use validation libraries like `zod` to securely import them during module initialization. diff --git a/packages/web/.env.example b/packages/web/.env.example index e99d51e..d056209 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -4,3 +4,5 @@ AUTH_SECRET=replace-with-min-32-char-random-string DATABASE_URL="postgresql://postgres.[project]:[password]@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true" DIRECT_URL="postgresql://postgres.[project]:[password]@db.uwxfseowdzuuepeeudrx.supabase.co:5432/postgres" JWT_SECRET="replace-with-32-char-minimum-random-string" +ADMIN_USERNAME=admin +ADMIN_PASSWORD=change-this-to-a-secure-password diff --git a/packages/web/src/lib/server/admin-auth.ts b/packages/web/src/lib/server/admin-auth.ts index 5390fa4..ea710b2 100644 --- a/packages/web/src/lib/server/admin-auth.ts +++ b/packages/web/src/lib/server/admin-auth.ts @@ -6,8 +6,8 @@ import { NextRequest, NextResponse } from 'next/server' import { env } from './env' -export const ADMIN_USERNAME = 'admin' -export const ADMIN_PASSWORD = 'og9oRajx7h88v1RIj3eDgdrh9jgLYVV3' +export const ADMIN_USERNAME = env.ADMIN_USERNAME +export const ADMIN_PASSWORD = env.ADMIN_PASSWORD const ADMIN_SESSION_COOKIE = 'argos_admin_session' const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000 diff --git a/packages/web/src/lib/server/env.ts b/packages/web/src/lib/server/env.ts index 86ed9a3..7eac191 100644 --- a/packages/web/src/lib/server/env.ts +++ b/packages/web/src/lib/server/env.ts @@ -5,6 +5,8 @@ const EnvSchema = z.object({ DATABASE_URL: z.string().min(1), DIRECT_URL: z.string().min(1), JWT_SECRET: z.string().min(32), + ADMIN_USERNAME: z.string().min(1), + ADMIN_PASSWORD: z.string().trim().min(8), }) export const env = EnvSchema.parse(process.env) From 8b82532864da1ecd284bed338cd5be9ecca20937 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:46:05 +0000 Subject: [PATCH 2/2] fix: secure admin password hashing and remove hardcoded credentials - Migrated hardcoded admin credentials to environment variables (`env.ts`). - Replaced insecure HMAC equality checking with secure, asynchronous PBKDF2 hashing in `verifyAdminCredentials`. - Pre-computed target password hashes via `pbkdf2Sync` on module init to avoid timing attacks on password length. - Refactored `api/admin/login/route.ts` to `await` the asynchronous verification. - Updated `.env.example` and CI workflow. --- .jules/sentinel.md | 5 ++++ packages/web/src/app/api/admin/login/route.ts | 2 +- packages/web/src/lib/server/admin-auth.ts | 25 +++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 477e86f..730cc39 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** A critical security vulnerability was found in `packages/web/src/lib/server/admin-auth.ts` where the admin username (`admin`) and admin password (`og9oRajx7h88v1RIj3eDgdrh9jgLYVV3`) were hardcoded in the source code. This is dangerous because anyone with access to the source code repository can easily extract these secrets and compromise the admin interface. **Learning:** Hardcoding credentials exposes sensitive data to unauthorized parties via source control systems. This practice circumvents the principle of least privilege. **Prevention:** Always define secrets in environment configuration systems (e.g. `.env`) and use validation libraries like `zod` to securely import them during module initialization. + +## 2025-02-14 - Insecure Password Hashing (js/insecure-password-hashing) +**Vulnerability:** A high severity CodeQL alert was triggered because passwords were hashed insecurely using fast HMAC functions before being compared via `timingSafeEqual`. This exposes the passwords to timing attacks or fast offline brute-forcing if the hashes are ever leaked or accessed. +**Learning:** Using simple or fast hash functions (like raw SHA256/HMAC) on passwords is an anti-pattern. Timing-safe equality checks using `crypto.timingSafeEqual()` still require the hash inputs to be generated by a strong, computationally expensive Key Derivation Function (KDF) like `pbkdf2` to resist brute-force attacks and prevent DoS vulnerabilities during comparison. +**Prevention:** Pre-compute target hashes at module initialization using synchronous `pbkdf2Sync`. When hashing incoming passwords within request handlers, use the asynchronous `crypto.pbkdf2` (via `util.promisify`) to prevent blocking the Node.js event loop and avoid DoS vulnerabilities. diff --git a/packages/web/src/app/api/admin/login/route.ts b/packages/web/src/app/api/admin/login/route.ts index dbf5d2e..24fabdb 100644 --- a/packages/web/src/app/api/admin/login/route.ts +++ b/packages/web/src/app/api/admin/login/route.ts @@ -20,7 +20,7 @@ const AdminLoginSchema = z.object({ export async function POST(req: Request) { try { const input = AdminLoginSchema.parse(await req.json()) - if (!verifyAdminCredentials(input)) { + if (!(await verifyAdminCredentials(input))) { return NextResponse.json({ error: 'Invalid username or password' }, { status: 401 }) } diff --git a/packages/web/src/lib/server/admin-auth.ts b/packages/web/src/lib/server/admin-auth.ts index ea710b2..a0248f1 100644 --- a/packages/web/src/lib/server/admin-auth.ts +++ b/packages/web/src/lib/server/admin-auth.ts @@ -1,7 +1,8 @@ import 'server-only' -import { createHmac, randomBytes, timingSafeEqual } from 'crypto' +import { createHmac, pbkdf2, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto' import { cookies } from 'next/headers' +import { promisify } from 'util' import { NextRequest, NextResponse } from 'next/server' import { env } from './env' @@ -14,6 +15,13 @@ const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000 const ADMIN_IMPERSONATION_TTL_MS = 60 * 1000 const ADMIN_IMPERSONATION_PREFIX = 'argos_imp' +const pbkdf2Async = promisify(pbkdf2) +const SALT = env.JWT_SECRET + +// Pre-compute target hashes to avoid timing attacks on password length +const TARGET_USERNAME_HASH = pbkdf2Sync(ADMIN_USERNAME, SALT, 100000, 64, 'sha512') +const TARGET_PASSWORD_HASH = pbkdf2Sync(ADMIN_PASSWORD, SALT, 100000, 64, 'sha512') + function safeEqual(a: string, b: string): boolean { const aHash = createHmac('sha256', env.JWT_SECRET).update(a).digest() const bHash = createHmac('sha256', env.JWT_SECRET).update(b).digest() @@ -24,14 +32,17 @@ function sign(payload: string): string { return createHmac('sha256', env.JWT_SECRET).update(payload).digest('base64url') } -export function verifyAdminCredentials(input: { +export async function verifyAdminCredentials(input: { username: string password: string -}): boolean { - return ( - safeEqual(input.username, ADMIN_USERNAME) && - safeEqual(input.password, ADMIN_PASSWORD) - ) +}): Promise { + const inputUsernameHash = await pbkdf2Async(input.username, SALT, 100000, 64, 'sha512') + const inputPasswordHash = await pbkdf2Async(input.password, SALT, 100000, 64, 'sha512') + + const isUsernameCorrect = timingSafeEqual(inputUsernameHash, TARGET_USERNAME_HASH) + const isPasswordCorrect = timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH) + + return isUsernameCorrect && isPasswordCorrect } export function createAdminSessionCookieValue(): string {