From 86ee111c859282c2ad537f58494792076b51c813 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:23:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20hardcoded=20admin=20credentials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .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..ce0c16f 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: "ci-admin" + ADMIN_PASSWORD: "ci-placeholder-password" diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..35c0371 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-06-11 - [CRITICAL] Fix hardcoded admin credentials +**Vulnerability:** Hardcoded admin credentials (`ADMIN_USERNAME` and `ADMIN_PASSWORD`) were found in `packages/web/src/lib/server/admin-auth.ts`. +**Learning:** Hardcoding credentials in source control can lead to unauthorized access if the repository is compromised or visible. The vulnerability was present in the auth logic, exposing the admin system. Moving credentials to environment variables (`.env`) via a schema validation tool (like Zod in `env.ts`) mitigates this issue while preserving API boundaries by exporting the env variables instead of hardcoded strings. +**Prevention:** Always use environment variables for sensitive credentials. Enforce strict checks for hardcoded strings resembling secrets during code review, and ensure environment variables are clearly defined with validation (e.g., Zod `min(1)` or similar length constraints) to prevent bypasses. diff --git a/packages/web/.env.example b/packages/web/.env.example index e99d51e..e0941da 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=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..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..221c6f0 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().min(1), }) export const env = EnvSchema.parse(process.env) From 03600f3a331cbf024109cc66ac999991ae26d399 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:00:00 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20insecure=20password=20hashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 5 ++++ packages/web/src/app/api/admin/login/route.ts | 3 +- packages/web/src/lib/server/admin-auth.ts | 30 ++++++++++++++----- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 35c0371..881313b 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** Hardcoded admin credentials (`ADMIN_USERNAME` and `ADMIN_PASSWORD`) were found in `packages/web/src/lib/server/admin-auth.ts`. **Learning:** Hardcoding credentials in source control can lead to unauthorized access if the repository is compromised or visible. The vulnerability was present in the auth logic, exposing the admin system. Moving credentials to environment variables (`.env`) via a schema validation tool (like Zod in `env.ts`) mitigates this issue while preserving API boundaries by exporting the env variables instead of hardcoded strings. **Prevention:** Always use environment variables for sensitive credentials. Enforce strict checks for hardcoded strings resembling secrets during code review, and ensure environment variables are clearly defined with validation (e.g., Zod `min(1)` or similar length constraints) to prevent bypasses. + +## 2024-06-12 - [CRITICAL] Fix insecure password hashing (CodeQL Alert) +**Vulnerability:** The initial fix used `createHmac` for `ADMIN_PASSWORD`, which triggered a CodeQL security alert for insecure password hashing (`js/insecure-password-hashing`). HMAC algorithms like SHA-256 are too fast and not designed for password hashing, making them susceptible to brute-force and dictionary attacks if the hashes leak or are targeted via timing attacks. +**Learning:** For password-like secrets, fast cryptographic functions or standard HMACs are insufficient. The `pbkdf2Sync` algorithm is required to generate a key securely at initialization, and `pbkdf2` via `util.promisify` should be used asynchronously to verify credentials without blocking the Node.js event loop (preventing DoS). +**Prevention:** Avoid using passwords directly as HMAC keys or hashing them with fast algorithms. Always use established cryptographic key derivation functions like `pbkdf2` with appropriate iteration counts (e.g., 100,000) for verifying secrets or passwords against stored hashes. diff --git a/packages/web/src/app/api/admin/login/route.ts b/packages/web/src/app/api/admin/login/route.ts index dbf5d2e..68c29fc 100644 --- a/packages/web/src/app/api/admin/login/route.ts +++ b/packages/web/src/app/api/admin/login/route.ts @@ -20,7 +20,8 @@ const AdminLoginSchema = z.object({ export async function POST(req: Request) { try { const input = AdminLoginSchema.parse(await req.json()) - if (!verifyAdminCredentials(input)) { + const isValid = await verifyAdminCredentials(input) + if (!isValid) { 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..c6a61e9 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, randomBytes, timingSafeEqual, pbkdf2Sync, pbkdf2 } from 'crypto' +import { promisify } from 'util' import { cookies } from 'next/headers' import { NextRequest, NextResponse } from 'next/server' @@ -14,6 +15,16 @@ const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000 const ADMIN_IMPERSONATION_TTL_MS = 60 * 1000 const ADMIN_IMPERSONATION_PREFIX = 'argos_imp' +// Pre-compute expected hashes at module initialization +const HASH_ITERATIONS = 100000 +const HASH_KEYLEN = 64 +const HASH_DIGEST = 'sha512' +// We use JWT_SECRET as a constant salt for the predefined credentials +const EXPECTED_USERNAME_HASH = pbkdf2Sync(ADMIN_USERNAME, env.JWT_SECRET, HASH_ITERATIONS, HASH_KEYLEN, HASH_DIGEST) +const EXPECTED_PASSWORD_HASH = pbkdf2Sync(ADMIN_PASSWORD, env.JWT_SECRET, HASH_ITERATIONS, HASH_KEYLEN, HASH_DIGEST) + +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 +35,19 @@ 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 [usernameHash, passwordHash] = await Promise.all([ + pbkdf2Async(input.username, env.JWT_SECRET, HASH_ITERATIONS, HASH_KEYLEN, HASH_DIGEST), + pbkdf2Async(input.password, env.JWT_SECRET, HASH_ITERATIONS, HASH_KEYLEN, HASH_DIGEST) + ]) + + const isUsernameCorrect = timingSafeEqual(usernameHash, EXPECTED_USERNAME_HASH) + const isPasswordCorrect = timingSafeEqual(passwordHash, EXPECTED_PASSWORD_HASH) + + return isUsernameCorrect && isPasswordCorrect } export function createAdminSessionCookieValue(): string {