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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 9 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 2025-06-06 - [CRITICAL] Fix hardcoded admin password

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

사건 λ‚ μ§œλ₯Ό PR νƒ€μž„λΌμΈκ³Ό μΌμΉ˜μ‹œν‚€μ„Έμš”.

Line 1의 λ‚ μ§œ(2025-06-06)κ°€ PR 생성일(2026-06-06)κ³Ό λΆˆμΌμΉ˜ν•©λ‹ˆλ‹€. λ³΄μ•ˆ 이λ ₯ 좔적 정확도λ₯Ό μœ„ν•΄ λ‚ μ§œλ₯Ό μ •μ •ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

πŸ€– 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 1, The sentinel entry header contains the wrong
date "2025-06-06" which should match the PR creation date; update the heading in
.jules/sentinel.md (the line starting "## 2025-06-06 - [CRITICAL] Fix hardcoded
admin password") to the correct date "2026-06-06" so the incident timestamp
aligns with the PR timeline.

**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.

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 | πŸ”΄ Critical | ⚑ Quick win

λ¬Έμ„œμ— μ‹€μ œ λΉ„λ°€λ²ˆν˜Έ λ¬Έμžμ—΄μ„ λ‹€μ‹œ 남기면 μ•ˆ λ©λ‹ˆλ‹€.

Line 2에 μ‹€μ œ κ΄€λ¦¬μž λΉ„λ°€λ²ˆν˜Έκ°€ κ·ΈλŒ€λ‘œ κΈ°λ‘λ˜μ–΄ μžˆμ–΄ μ €μž₯μ†Œ λ‚΄ λΉ„λ°€ λ…ΈμΆœμ΄ μ§€μ†λ©λ‹ˆλ‹€. μ¦‰μ‹œ λ§ˆμŠ€ν‚Ήν•˜κ³ (ν•„μš” μ‹œ) ν•΄λ‹Ή 자격증λͺ… νšŒμ „ μ—¬λΆ€λ₯Ό ν•¨κ»˜ κΈ°λ‘ν•˜μ„Έμš”.

πŸ”§ μ œμ•ˆ 패치
-**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.
+**Vulnerability:** A hardcoded admin password (`<redacted>`) was present in `packages/web/src/lib/server/admin-auth.ts`, exposing admin credentials in source code.
πŸ“ 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
**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.
**Vulnerability:** A hardcoded admin password (`<redacted>`) was present in `packages/web/src/lib/server/admin-auth.ts`, exposing admin credentials in source code.
πŸ€– 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, λ¬Έμ„œμ— λ…ΈμΆœλœ μ‹€μ œ κ΄€λ¦¬μž λΉ„λ°€λ²ˆν˜Έ λ¬Έμžμ—΄μ„ μ¦‰μ‹œ μ œκ±°ν•˜κ³  λ§ˆμŠ€ν‚Ήν•˜μ„Έμš”:
`.jules/sentinel.md`의 2행에 μžˆλŠ” λΉ„λ°€λ²ˆν˜Έ 값을 μ‚­μ œν•˜κ±°λ‚˜ `*****`/`<REDACTED>`둜 λŒ€μ²΄ν•˜κ³ , λ…ΈμΆœλœ 자격증λͺ…은
`packages/web/src/lib/server/admin-auth.ts`μ—μ„œ ν•˜λ“œμ½”λ”©λœ λΉ„λ°€λ²ˆν˜Έ(ν•˜λ“œμ½”λ”© μƒμˆ˜ λ˜λŠ” ν™˜κ²½λ³€μˆ˜ μ„€μ •)λ₯Ό
κ΅μ²΄ν•˜κ³  λΉ„λ°€ νšŒμ „ κ³„νšμ„ κΈ°λ‘ν•˜λ„λ‘ λ¬Έμ„œμ— 짧게 λ§λΆ™μ΄μ„Έμš”; λ˜ν•œ λΉ„λ°€λ²ˆν˜Έλ₯Ό κ°±μ‹ ν–ˆλ‹€λ©΄ ν•΄λ‹Ή ꡐ체 μž‘μ—…(예: μƒˆ λΉ„λ°€λ²ˆν˜Έ λ°œκΈ‰/μ‹œν¬λ¦Ώ
λ‘œν…Œμ΄μ…˜)κ³Ό κ΄€λ ¨λœ 쑰치 및 κΆŒν•œ 철회 절차λ₯Ό λ¬Έμ„œμ— λͺ…μ‹œν•΄ 감사 좔적이 κ°€λŠ₯ν•˜λ„λ‘ ν•˜μ„Έμš”.

**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.
1 change: 1 addition & 0 deletions packages/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
36 changes: 29 additions & 7 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
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'

import { env } from './env'

export const ADMIN_USERNAME = 'admin'
export const ADMIN_PASSWORD = 'og9oRajx7h88v1RIj3eDgdrh9jgLYVV3'
export const ADMIN_PASSWORD = env.ADMIN_PASSWORD
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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'

// 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()
Expand All @@ -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<boolean> {
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 {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/lib/server/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading