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"
ADMIN_PASSWORD: "ci-placeholder-password"
9 changes: 9 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 2026-06-08 - [Hardcoded Admin Password]
**Vulnerability:** Hardcoded `ADMIN_USERNAME` and `ADMIN_PASSWORD` in `packages/web/src/lib/server/admin-auth.ts` could allow unauthorized access if source code is exposed.
**Learning:** Hardcoding credentials makes the application insecure and inflexible, and prevents credential rotation without code deployment.
**Prevention:** Use environment variables validated via schema (e.g., Zod) for all credentials and API keys. Keep default values secure and avoid committing secrets.

## 2026-06-08 - [Insecure Password Hashing]
**Vulnerability:** Fast hash (HMAC-SHA256) was used for timing-safe equality checks of passwords, which triggered a CodeQL `js/insecure-password-hashing` alert.
**Learning:** Using simple HMACs for passwords is not enough against offline dictionary attacks if the secret is known or short. CodeQL expects a proper key derivation function (like `pbkdf2`) for passwords. Furthermore, sync versions of KDFs (like `pbkdf2Sync`) should not be used in request paths as they block the event loop and cause DoS.
**Prevention:** Precompute target hashes at module initialization using `pbkdf2Sync`, and use the asynchronous `crypto.pbkdf2` (via `util.promisify`) for hashing incoming passwords within request handlers.
2 changes: 2 additions & 0 deletions packages/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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-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
25 changes: 16 additions & 9 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import 'server-only'

import { createHmac, randomBytes, timingSafeEqual } from 'crypto'
import { createHmac, pbkdf2, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import { promisify } from 'util'

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

// Precompute target hash to resolve CodeQL js/insecure-password-hashing alert
// using sync method at module initialization time.
const ADMIN_PASSWORD_HASH = pbkdf2Sync(ADMIN_PASSWORD, env.JWT_SECRET, 100000, 64, 'sha512')
const pbkdf2Async = promisify(pbkdf2)

const ADMIN_SESSION_COOKIE = 'argos_admin_session'
const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000
Expand All @@ -24,14 +30,15 @@ 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

// Use async pbkdf2 to avoid blocking the event loop when verifying input password
const inputHash = await pbkdf2Async(input.password, env.JWT_SECRET, 100000, 64, 'sha512')
return timingSafeEqual(inputHash, ADMIN_PASSWORD_HASH)
}

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),

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 | ⚑ Quick win

ADMIN_USERNAME에 . ν—ˆμš© μ‹œ κ΄€λ¦¬μž μ„Έμ…˜ 검증이 κΉ¨μ§‘λ‹ˆλ‹€.

packages/web/src/lib/server/admin-auth.ts의 μ„Έμ…˜ 값은 username.expiresAt.nonce.signature ν˜•νƒœλ‘œ μƒμ„±λ˜κ³ (Line 40), 검증 μ‹œ split('.') κ²°κ³Όλ₯Ό 4개둜 κ³ μ • κ²€μ‚¬ν•©λ‹ˆλ‹€(Line 64).
ν˜„μž¬ μŠ€ν‚€λ§ˆλŠ” ADMIN_USERNAME에 .λ₯Ό ν—ˆμš©ν•˜λ―€λ‘œ(예: admin.ops), 정상 둜그인 후에도 μ„Έμ…˜μ΄ 항상 무효 처리될 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ”§ μ œμ•ˆ μˆ˜μ •
 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_USERNAME: z
+    .string()
+    .trim()
+    .min(1)
+    .regex(/^[^.]+$/, 'ADMIN_USERNAME must not contain "."'),
   ADMIN_PASSWORD: z.string().trim().min(8),
 })
πŸ€– 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/env.ts` at line 8, Session validation breaks when
ADMIN_USERNAME contains '.' because admin session format in admin-auth.ts is
username.expiresAt.nonce.signature and validation splits on '.' expecting 4
parts; update the ADMIN_USERNAME schema in env.ts to forbid dots (e.g., require
no '.' via a regex or refine) so usernames like "admin.ops" cannot be created,
and ensure the change references ADMIN_USERNAME and the session
creation/validation logic in admin-auth.ts to keep the 4-part split invariant.

ADMIN_PASSWORD: z.string().trim().min(8),
})

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