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-06-10 - [Hardcoded Admin Password]
**Vulnerability:** A hardcoded admin password `og9oRajx7h88v1RIj3eDgdrh9jgLYVV3` was found in `packages/web/src/lib/server/admin-auth.ts`.
**Learning:** Hardcoded credentials in source code pose a critical security risk because anyone with access to the source code (or a compiled version of it) can extract the credentials and use them to gain unauthorized access to the application.
**Prevention:** Always use environment variables for sensitive data like passwords, API keys, and secrets. Ensure they are validated securely at runtime using tools like Zod (e.g., in `env.ts`) and provide dummy values in `.env.example` and CI workflows to maintain a robust and secure development/deployment pipeline.
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=change-me-in-production
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
16 changes: 11 additions & 5 deletions packages/web/src/lib/server/admin-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
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_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)
const ADMIN_PASSWORD_HASH = pbkdf2Sync(env.ADMIN_PASSWORD, env.JWT_SECRET, 100000, 64, 'sha256')

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,13 +28,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 {
}): Promise<boolean> {
const passHash = await pbkdf2Async(input.password, env.JWT_SECRET, 100000, 64, 'sha256')

return (
safeEqual(input.username, ADMIN_USERNAME) &&
safeEqual(input.password, ADMIN_PASSWORD)
timingSafeEqual(passHash, 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().min(8),
})

export const env = EnvSchema.parse(process.env)
50 changes: 50 additions & 0 deletions update-auth.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<<<<<<< SEARCH
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()
return timingSafeEqual(aHash, bHash)
}

function sign(payload: string): string {
return createHmac('sha256', env.JWT_SECRET).update(payload).digest('base64url')
}

export function verifyAdminCredentials(input: {
username: string
password: string
}): boolean {
return (
safeEqual(input.username, ADMIN_USERNAME) &&
safeEqual(input.password, ADMIN_PASSWORD)
)
}
=======
import { pbkdf2Sync, pbkdf2 } from 'crypto'
import { promisify } from 'util'

const pbkdf2Async = promisify(pbkdf2)

const ADMIN_PASSWORD_HASH = pbkdf2Sync(env.ADMIN_PASSWORD, env.JWT_SECRET, 100000, 64, 'sha256')

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()
return timingSafeEqual(aHash, bHash)
}

function sign(payload: string): string {
return createHmac('sha256', env.JWT_SECRET).update(payload).digest('base64url')
}

export async function verifyAdminCredentials(input: {
username: string
password: string
}): Promise<boolean> {
const passHash = await pbkdf2Async(input.password, env.JWT_SECRET, 100000, 64, 'sha256')

return (
safeEqual(input.username, ADMIN_USERNAME) &&
timingSafeEqual(passHash, ADMIN_PASSWORD_HASH)
)
}
>>>>>>> REPLACE
Loading