Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-user"
ADMIN_PASSWORD: "ci-placeholder-admin-password-min-8-chars"
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

## 2024-05-24 - [CRITICAL] Fix Hardcoded Admin Credentials in Authentication Logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor | โšก Quick win

์„ผํ‹ฐ๋„ฌ ๊ธฐ๋ก์˜ ๋‚ ์งœ๋ฅผ ์ˆ˜์ •ํ•˜์„ธ์š”.

๋ฌธ์„œ์— ๊ธฐ๋ก๋œ ๋‚ ์งœ๊ฐ€ 2024-05-24์ด์ง€๋งŒ ์ด PR์€ 2026-06-07์— ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„ผํ‹ฐ๋„ฌ ๊ธฐ๋ก์˜ ๋‚ ์งœ๋Š” ์‹ค์ œ ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์ง„ ๋‚ ์งœ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๋ฐ˜์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ ๋‚ ์งœ ์ˆ˜์ • ์ œ์•ˆ
-## 2024-05-24 - [CRITICAL] Fix Hardcoded Admin Credentials in Authentication Logic
+## 2026-06-07 - [CRITICAL] Fix Hardcoded Admin Credentials in Authentication Logic
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 2024-05-24 - [CRITICAL] Fix Hardcoded Admin Credentials in Authentication Logic
## 2026-06-07 - [CRITICAL] Fix Hardcoded Admin Credentials in Authentication Logic
๐Ÿค– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/sentinel.md at line 2, Update the sentinel entry heading that
currently reads "## 2024-05-24 - [CRITICAL] Fix Hardcoded Admin Credentials in
Authentication Logic" so the date reflects the actual PR creation/fix date
(change 2024-05-24 to 2026-06-07); locate and edit the header line in
.jules/sentinel.md (the "## ... - [CRITICAL] Fix Hardcoded Admin Credentials in
Authentication Logic" line) to use the corrected date.

**Vulnerability:** Found hardcoded `ADMIN_USERNAME` and `ADMIN_PASSWORD` in `packages/web/src/lib/server/admin-auth.ts`.
**Learning:** Hardcoding credentials in source code exposes them to anyone with read access to the repository and makes it impossible to securely manage or rotate these secrets across different environments.
**Prevention:** Always use environment variables for secrets and credentials. Use tools like `zod` to validate their presence at runtime (e.g., in `env.ts`) and ensure proper placeholder values are added to `.env.example` and CI workflows to prevent build regressions.
4 changes: 4 additions & 0 deletions packages/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ 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 credentials
ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-secure-admin-password
2 changes: 1 addition & 1 deletion packages/web/src/app/api/admin/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}

Expand Down
44 changes: 36 additions & 8 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
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'

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
const ADMIN_IMPERSONATION_TTL_MS = 60 * 1000
const ADMIN_IMPERSONATION_PREFIX = 'argos_imp'

const pbkdf2Async = promisify(pbkdf2)

// PBKDF2 parameters for secure password hashing
const HASH_SALT = 'argos_admin_salt'
const HASH_ITERATIONS = 100000
const HASH_KEYLEN = 64
const HASH_DIGEST = 'sha512'
Comment on lines +20 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major | ๐Ÿ—๏ธ Heavy lift

์ •์  ์†”ํŠธ ์‚ฌ์šฉ์€ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ์œ„๋ฐ˜

HASH_SALT๊ฐ€ ์†Œ์Šค ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ ์†Œ์Šค ์ฝ”๋“œ์— ์ ‘๊ทผํ•˜๋ฉด ์ด ์†”ํŠธ๋กœ ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”์„ ์‚ฌ์ „ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ถŒ์žฅ ํ•ด๊ฒฐ์ฑ…:

  • ADMIN_PASSWORD_SALT ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๋ฐฐํฌ ์‹œ ๋žœ๋ค ์ƒ์„ฑ๋œ ๊ฐ’ ์‚ฌ์šฉ
  • ๋˜๋Š” ADMIN_PASSWORD ๋Œ€์‹  ์ด๋ฏธ ํ•ด์‹œ๋œ ADMIN_PASSWORD_HASH์™€ ์†”ํŠธ๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ €์žฅ

๋‹จ์ผ ๊ด€๋ฆฌ์ž ๊ณ„์ •์— 100k iterations๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ฆ‰๊ฐ์ ์ธ ์œ„ํ˜‘์€ ์•„๋‹ˆ์ง€๋งŒ, ๋ณด์•ˆ ์‹ฌ์ธต ๋ฐฉ์–ด ์ธก๋ฉด์—์„œ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ›ก๏ธ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ ์†”ํŠธ ์‚ฌ์šฉ ์ œ์•ˆ

packages/web/src/lib/server/env.ts์— ์ถ”๊ฐ€:

 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().trim().min(1),
   ADMIN_PASSWORD: z.string().trim().min(8),
+  ADMIN_PASSWORD_SALT: z.string().min(16),
 })

admin-auth.ts ์ˆ˜์ •:

-const HASH_SALT = 'argos_admin_salt'
+const HASH_SALT = env.ADMIN_PASSWORD_SALT
๐Ÿค– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/server/admin-auth.ts` around lines 20 - 24, Replace the
hardcoded HASH_SALT in admin-auth.ts with an environment-driven salt: add an
ADMIN_PASSWORD_SALT (or ADMIN_PASSWORD_HASH+ADMIN_PASSWORD_SALT) to the app env
module (e.g., export it from packages/web/src/lib/server/env.ts) and update the
code paths that reference HASH_SALT to read the salt from that env variable; if
you choose to store ADMIN_PASSWORD_HASH instead of plain ADMIN_PASSWORD, adapt
the authentication routines (the PBKDF2/verify logic in admin-auth.ts that uses
HASH_ITERATIONS, HASH_KEYLEN, HASH_DIGEST) to compare against the provided hash
and use the env salt, and ensure a clear runtime error is thrown when the
required env variables are missing.


// Pre-compute the target hash of the admin password at module initialization
const TARGET_PASSWORD_HASH = pbkdf2Sync(
ADMIN_PASSWORD,
HASH_SALT,
HASH_ITERATIONS,
HASH_KEYLEN,
HASH_DIGEST
)

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()
Expand All @@ -24,14 +42,24 @@ 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<boolean> {
if (!safeEqual(input.username, ADMIN_USERNAME)) {
return false
}

// Hash the incoming password asynchronously to avoid blocking the event loop
const inputPasswordHash = await pbkdf2Async(
input.password,
HASH_SALT,
HASH_ITERATIONS,
HASH_KEYLEN,
HASH_DIGEST
)

return timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH)
Comment on lines +49 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor | โšก Quick win

ํƒ€์ด๋ฐ ์˜ค๋ผํด์„ ํ†ตํ•œ ์‚ฌ์šฉ์ž๋ช… ์—ด๊ฑฐ ๊ฐ€๋Šฅ์„ฑ

์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜ ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜๊ณ (Line 49-51), ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ์€ PBKDF2 ํ•ด์‹œ ๊ณ„์‚ฐ ํ›„ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ์‹œ๊ฐ„ ์ฐจ์ด(~์ˆ˜๋ฐฑms)๋กœ ๊ณต๊ฒฉ์ž๊ฐ€ ์œ ํšจํ•œ ์‚ฌ์šฉ์ž๋ช…์„ ์—ด๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ผ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์‹œ์Šคํ…œ์—์„œ๋Š” ์œ„ํ—˜๋„๊ฐ€ ๋‚ฎ์ง€๋งŒ, ์™„์ „ํ•œ ์ƒ์ˆ˜ ์‹œ๊ฐ„ ๊ฒ€์ฆ์„ ์œ„ํ•ด ์‚ฌ์šฉ์ž๋ช…๊ณผ ๊ด€๊ณ„์—†์ด ํ•ญ์ƒ PBKDF2๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

โฑ๏ธ ์ƒ์ˆ˜ ์‹œ๊ฐ„ ๊ฒ€์ฆ ์ œ์•ˆ
 export async function verifyAdminCredentials(input: {
   username: string
   password: string
 }): Promise<boolean> {
-  if (!safeEqual(input.username, ADMIN_USERNAME)) {
-    return false
-  }
-
   // Hash the incoming password asynchronously to avoid blocking the event loop
   const inputPasswordHash = await pbkdf2Async(
     input.password,
     HASH_SALT,
     HASH_ITERATIONS,
     HASH_KEYLEN,
     HASH_DIGEST
   )
 
-  return timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH)
+  const usernameMatch = safeEqual(input.username, ADMIN_USERNAME)
+  const passwordMatch = timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH)
+  return usernameMatch && passwordMatch
 }
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!safeEqual(input.username, ADMIN_USERNAME)) {
return false
}
// Hash the incoming password asynchronously to avoid blocking the event loop
const inputPasswordHash = await pbkdf2Async(
input.password,
HASH_SALT,
HASH_ITERATIONS,
HASH_KEYLEN,
HASH_DIGEST
)
return timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH)
// Hash the incoming password asynchronously to avoid blocking the event loop
const inputPasswordHash = await pbkdf2Async(
input.password,
HASH_SALT,
HASH_ITERATIONS,
HASH_KEYLEN,
HASH_DIGEST
)
const usernameMatch = safeEqual(input.username, ADMIN_USERNAME)
const passwordMatch = timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH)
return usernameMatch && passwordMatch
๐Ÿค– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/server/admin-auth.ts` around lines 49 - 62, Always run
the PBKDF2 hashing regardless of the username match to avoid timing leakage:
call pbkdf2Async(...) unconditionally, compute passwordMatch =
timingSafeEqual(inputPasswordHash, TARGET_PASSWORD_HASH), compute usernameMatch
= safeEqual(input.username, ADMIN_USERNAME), then return usernameMatch &&
passwordMatch (ensure inputPasswordHash is computed before those comparisons).
Reference pbkdf2Async, timingSafeEqual, safeEqual, ADMIN_USERNAME,
TARGET_PASSWORD_HASH, and the HASH_* constants when making the change.

}

export function createAdminSessionCookieValue(): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/lib/server/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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().trim().min(1),
ADMIN_PASSWORD: z.string().trim().min(8),
})

export const env = EnvSchema.parse(process.env)
Loading