From 9c153f21beccf883f0b297fe11055c2ae6962068 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 23:42:36 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=206=20bugs=20=E2=80=94=20passwor?= =?UTF-8?q?d=20hashing,=20SQL=20injection=20risk,=20JWT=20inconsistency,?= =?UTF-8?q?=20and=20count=20query=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/api/users/route.ts: hash passwords with bcrypt before storing; fix count query sending wrong params when no search filter is active (passed `[limit]` to a parameterless SQL statement, causing a better-sqlite3 error) - app/api/users/[id]/route.ts: hash password with bcrypt on user update - app/api/sessions/[id]/route.ts: replace double-quoted SQL string literal with a parameterised binding (SQLite treats double-quotes as identifiers) - app/api/client/login/route.ts: replace inconsistent raw JWT_SECRET check (min 64 chars) with getJwtSecret() so client and admin tokens share the same secret derivation logic - lib/db.ts: catch and log startup validateConnection() errors instead of silently dropping them https://claude.ai/code/session_01DkUApogQMjHvzsmsEty9z7 --- app/api/client/login/route.ts | 8 ++------ app/api/sessions/[id]/route.ts | 2 +- app/api/users/[id]/route.ts | 3 ++- app/api/users/route.ts | 9 ++++++--- lib/db.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/api/client/login/route.ts b/app/api/client/login/route.ts index 29caed9..e6cd2c8 100644 --- a/app/api/client/login/route.ts +++ b/app/api/client/login/route.ts @@ -3,6 +3,7 @@ import { headers } from 'next/headers'; import { query } from '@/lib/db'; import bcrypt from 'bcryptjs'; import * as jose from 'jose'; +import { getJwtSecret } from '@/lib/auth-utils'; import { isRateLimited } from '@/lib/rate-limit'; import { auditLog } from '@/lib/audit-logger'; @@ -31,12 +32,7 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); } - if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 64) { - throw new Error("JWT_SECRET missing or too weak (min 64 chars required)"); - } - - // Generate JWT using jose - const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const secret = await getJwtSecret(); const token = await new jose.SignJWT({ id: user.id, username: user.username }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() diff --git a/app/api/sessions/[id]/route.ts b/app/api/sessions/[id]/route.ts index 8fec119..edc43ae 100644 --- a/app/api/sessions/[id]/route.ts +++ b/app/api/sessions/[id]/route.ts @@ -10,7 +10,7 @@ export async function DELETE( const { id } = await params; if (!id) throw new Error('ID required'); - await query('UPDATE sessions SET status = "disconnected", end_time = CURRENT_TIMESTAMP WHERE id = ?', [id]); + await query('UPDATE sessions SET status = ?, end_time = CURRENT_TIMESTAMP WHERE id = ?', ['disconnected', id]); return NextResponse.json({ success: true }); } catch (error) { return handleApiError(error); diff --git a/app/api/users/[id]/route.ts b/app/api/users/[id]/route.ts index ebf8849..1c7f8a1 100644 --- a/app/api/users/[id]/route.ts +++ b/app/api/users/[id]/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; +import bcrypt from 'bcryptjs'; import pool from '@/lib/db'; import { auditLog } from '@/lib/audit-logger'; @@ -92,7 +93,7 @@ export async function PATCH( if (validatedData.data.password) { updates.push('password_hash = ?'); - values.push(validatedData.data.password); + values.push(await bcrypt.hash(validatedData.data.password, 10)); } if (updates.length === 0) { diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 062061a..04fccbd 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; +import bcrypt from 'bcryptjs'; import pool from '@/lib/db'; import { auditLog } from '@/lib/audit-logger'; @@ -58,7 +59,8 @@ export async function GET(request: Request) { params.push(limit, offset); const [rows] = await pool.execute(sql, params); - const [countResult]: any = await pool.execute(countSql, params.slice(0, 1)); + const countParams = search ? [`%${search}%`] : []; + const [countResult]: any = await pool.execute(countSql, countParams); const total = countResult[0].total; return NextResponse.json({ @@ -104,12 +106,13 @@ export async function POST(request: Request) { // Convert empty string from date input to null const finalExpiresAt = expires_at ? new Date(expires_at) : null; - // In a real app, hash password here + const hashedPassword = password ? await bcrypt.hash(password, 10) : null; + const [result]: any = await pool.execute( `INSERT INTO vpn_users (username, password_hash, role, status, traffic_limit_gb, max_connections, expires_at, cisco_password, l2tp_password, wg_pubkey, xray_uuid, port, main_protocol) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [username, password || null, role, status, traffic_limit_gb, max_connections, finalExpiresAt, cisco_password || null, l2tp_password || null, wg_pubkey || null, xray_uuid || null, port || null, main_protocol || null] + [username, hashedPassword, role, status, traffic_limit_gb, max_connections, finalExpiresAt, cisco_password || null, l2tp_password || null, wg_pubkey || null, xray_uuid || null, port || null, main_protocol || null] ); const userId = result.insertId; diff --git a/lib/db.ts b/lib/db.ts index 360d757..4b20a3e 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -58,7 +58,7 @@ export async function validateConnection() { // Ensure connection is validated on startup if (process.env.NODE_ENV !== 'test') { - validateConnection(); + validateConnection().catch(err => console.error('[db] Failed to initialize database:', err)); } /**