From dfde2bebebcbe9ea72c8d7e87de5ba7872d20651 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:15:53 +0000 Subject: [PATCH 1/2] fix: remove hardcoded admin password --- .github/workflows/ci.yml | 1 + .jules/sentinel.md | 4 ++++ packages/web/.env.example | 1 + packages/web/src/lib/server/admin-auth.ts | 2 +- packages/web/src/lib/server/env.ts | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .jules/sentinel.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc24903..10dae18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,4 @@ 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_PASSWORD: "ci-placeholder-admin-password" diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..b84e30a --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-06-06 - [CRITICAL] Fix hardcoded admin password +**Vulnerability:** A hardcoded admin password (`og9oRajx7h88v1RIj3eDgdrh9jgLYVV3`) was present in `packages/web/src/lib/server/admin-auth.ts`, exposing the admin credentials in the source code. +**Learning:** Hardcoded credentials can easily be checked into version control and compromise security. They should be loaded via environment variables and validated at runtime using tools like `zod`. +**Prevention:** Use environment variables for all secrets, ensure they are validated by the configuration loader (e.g. `env.ts`), and maintain proper `.env.example` templates and CI placeholder values so developers and automation understand the requirements. diff --git a/packages/web/.env.example b/packages/web/.env.example index e99d51e..73368e6 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -4,3 +4,4 @@ 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_PASSWORD=replace-with-secure-admin-password diff --git a/packages/web/src/lib/server/admin-auth.ts b/packages/web/src/lib/server/admin-auth.ts index 5390fa4..044fea7 100644 --- a/packages/web/src/lib/server/admin-auth.ts +++ b/packages/web/src/lib/server/admin-auth.ts @@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server' import { env } from './env' export const ADMIN_USERNAME = 'admin' -export const ADMIN_PASSWORD = 'og9oRajx7h88v1RIj3eDgdrh9jgLYVV3' +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..8e6fc2f 100644 --- a/packages/web/src/lib/server/env.ts +++ b/packages/web/src/lib/server/env.ts @@ -5,6 +5,7 @@ const EnvSchema = z.object({ DATABASE_URL: z.string().min(1), DIRECT_URL: z.string().min(1), JWT_SECRET: z.string().min(32), + ADMIN_PASSWORD: z.string().min(8), }) export const env = EnvSchema.parse(process.env) From 53f0749bfbfd49873a0dae1fec956974aa1694fd Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:25:55 +0000 Subject: [PATCH 2/2] fix: remove hardcoded admin password and resolve insecure hashing --- .jules/sentinel.md | 5 +++ packages/web/src/app/api/admin/login/route.ts | 2 +- packages/web/src/lib/server/admin-auth.ts | 34 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index b84e30a..a234405 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** A hardcoded admin password (`og9oRajx7h88v1RIj3eDgdrh9jgLYVV3`) was present in `packages/web/src/lib/server/admin-auth.ts`, exposing the admin credentials in the source code. **Learning:** Hardcoded credentials can easily be checked into version control and compromise security. They should be loaded via environment variables and validated at runtime using tools like `zod`. **Prevention:** Use environment variables for all secrets, ensure they are validated by the configuration loader (e.g. `env.ts`), and maintain proper `.env.example` templates and CI placeholder values so developers and automation understand the requirements. + +## 2025-06-06 - [CRITICAL] Fix CodeQL Insecure Password Hashing Alert +**Vulnerability:** `verifyAdminCredentials` used `createHmac` to verify passwords via `safeEqual`, which triggered a CodeQL alert for insecure password hashing (js/insecure-password-hashing). Custom 'homebrew' buffer-padding or direct HMAC was vulnerable to timing attacks. +**Learning:** When fixing CodeQL alerts for insecure password hashing, standard HMAC-based comparisons should not be used for direct password validation. Instead, use an established cryptographic method like pbkdf2. To prevent blocking the Node.js event loop during API requests, use the asynchronous `crypto.pbkdf2` via `util.promisify`. For target hash, use `pbkdf2Sync` pre-computed at module initialization. +**Prevention:** Rely on established cryptographic methods like pbkdf2 and ensure they are executed asynchronously in route handlers to avoid Denial of Service 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 044fea7..d2086f9 100644 --- a/packages/web/src/lib/server/admin-auth.ts +++ b/packages/web/src/lib/server/admin-auth.ts @@ -1,6 +1,7 @@ import 'server-only' -import { createHmac, randomBytes, timingSafeEqual } from 'crypto' +import { createHmac, pbkdf2Sync, pbkdf2, randomBytes, timingSafeEqual } from 'crypto' +import { promisify } from 'util' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' @@ -14,6 +15,18 @@ const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000 const ADMIN_IMPERSONATION_TTL_MS = 60 * 1000 const ADMIN_IMPERSONATION_PREFIX = 'argos_imp' +// Pre-compute the target hash for the admin password to prevent timing attacks. +// We use pbkdf2Sync since it's at module initialization. +const ADMIN_PASSWORD_TARGET_HASH = pbkdf2Sync( + ADMIN_PASSWORD, + env.JWT_SECRET, // using JWT_SECRET as salt for the in-memory hash + 100000, + 64, + 'sha512' +) + +const pbkdf2Async = promisify(pbkdf2) + 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 +37,23 @@ 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 { + if (!safeEqual(input.username, ADMIN_USERNAME)) { + return false + } + + const inputPasswordHash = await pbkdf2Async( + input.password, + env.JWT_SECRET, + 100000, + 64, + 'sha512' ) + + return timingSafeEqual(inputPasswordHash, ADMIN_PASSWORD_TARGET_HASH) } export function createAdminSessionCookieValue(): string {