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"
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-24 - 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

λ³΄μ•ˆ 이벀트 λ‚ μ§œλ₯Ό μ‹€μ œ λ°œμƒ/쑰치 μ‹œμ μœΌλ‘œ μ •μ •ν•˜μ„Έμš”.

Line 1의 2024-05-24λŠ” ν˜„μž¬ PR 메타데이터(2026-06-05)와 λ§žμ§€ μ•Šμ•„ 감사 좔적 μ‹œ ν˜Όμ„ μ„ μ€λ‹ˆλ‹€. λ¬Έμ„œμ—λŠ” μ‹€μ œ 발견일/μ‘°μΉ˜μΌμ„ λͺ…μ‹œν•΄ νƒ€μž„λΌμΈμ„ μΌκ΄€λ˜κ²Œ μœ μ§€ν•˜λŠ” 편이 μ’‹μŠ΅λ‹ˆλ‹€.

πŸ€– 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, Update the heading "## 2024-05-24 - Hardcoded
Admin Password" to reflect the actual discovery/mitigation date used in this PR
(e.g., change 2024-05-24 to the real event date such as 2026-06-05) so the
sentinel.md timeline matches PR metadata; ensure the date format remains
"YYYY-MM-DD" and that both discovery and remediation dates are corrected if
present elsewhere in the file.

**Vulnerability:** Found a hardcoded `ADMIN_PASSWORD` secret in `packages/web/src/lib/server/admin-auth.ts`.
**Learning:** Hardcoding credentials makes them accessible in source control and compiled assets, leading to severe exposure.
**Prevention:** Store secrets as environment variables, validate their presence at startup using tools like Zod, and inject them via CI/CD pipelines instead of embedding them directly in source code.
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
35 changes: 28 additions & 7 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import 'server-only'

import { createHmac, randomBytes, timingSafeEqual } from 'crypto'
import { createHmac, pbkdf2Sync, pbkdf2, 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_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 admin password hash at startup to avoid slow, synchronous operations
// later and to satisfy CodeQL's secure password hashing rule.
const ADMIN_PASSWORD_SALT = env.JWT_SECRET.substring(0, 16)
const EXPECTED_ADMIN_PASSWORD_HASH = pbkdf2Sync(
env.ADMIN_PASSWORD,
ADMIN_PASSWORD_SALT,
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 +38,21 @@ 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 inputHash = await pbkdf2Async(
input.password,
ADMIN_PASSWORD_SALT,
100000,
64,
'sha512'
)

return timingSafeEqual(inputHash, EXPECTED_ADMIN_PASSWORD_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