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 @@
## 2025-06-03 - Avoid Hardcoded Passwords in Source Code
**Vulnerability:** A hardcoded admin password was found directly embedded in the source code (`packages/web/src/lib/server/admin-auth.ts`).
**Learning:** Hardcoded secrets (passwords, API keys, tokens) in source code pose a critical security risk as they can be easily extracted by anyone with access to the codebase or the compiled artifacts. In this project, even static authentication constants like `ADMIN_PASSWORD` should be securely managed through environment variables to ensure secrets never reach the repository.
**Prevention:** Always use `process.env` (or a validated schema like Zod's `env`) to read secrets. Ensure all required secrets are documented in `.env.example` and that CI pipelines provide placeholder values when these variables are required for build time schemas.
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
16 changes: 12 additions & 4 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import 'server-only'

import { createHmac, randomBytes, timingSafeEqual } from 'crypto'
import { createHmac, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto'
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'

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 at module initialization to avoid synchronous work in the request path
const ADMIN_PASSWORD_HASH = pbkdf2Sync(env.ADMIN_PASSWORD, env.JWT_SECRET, 1000, 64, 'sha512')
const ADMIN_USERNAME_HASH = pbkdf2Sync(ADMIN_USERNAME, env.JWT_SECRET, 1000, 64, 'sha512')

function safeEqualHash(input: string, targetHash: Buffer): boolean {
const inputHash = pbkdf2Sync(input, env.JWT_SECRET, 1000, 64, 'sha512')
return timingSafeEqual(inputHash, targetHash)
}

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 @@ -29,8 +37,8 @@ export function verifyAdminCredentials(input: {
password: string
}): boolean {
return (
safeEqual(input.username, ADMIN_USERNAME) &&
safeEqual(input.password, ADMIN_PASSWORD)
safeEqualHash(input.username, ADMIN_USERNAME_HASH) &&
safeEqualHash(input.password, ADMIN_PASSWORD_HASH)
)
}

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().trim().min(8),
})

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