Add enhanced security: validate table names, require JWT_SECRET, improve query validation

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 01:29:48 +00:00
parent 8765b6c589
commit e4ec2b7d18
4 changed files with 71 additions and 12 deletions

4
.env
View File

@@ -17,6 +17,10 @@ NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# `DATABASE_URL` is a placeholder, you can find your connection string in `.env.local` file. # `DATABASE_URL` is a placeholder, you can find your connection string in `.env.local` file.
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres
# Admin Panel JWT Secret (REQUIRED for admin panel authentication)
# Generate a secure secret with: openssl rand -base64 32
JWT_SECRET=your-secret-key-change-in-production
# Next.js # Next.js
NEXT_TELEMETRY_DISABLED=1 NEXT_TELEMETRY_DISABLED=1

View File

@@ -93,19 +93,18 @@ export default function AdminDashboard() {
setQueryResult(null); setQueryResult(null);
try { try {
// Use parameterized query through the query API to prevent SQL injection // Use dedicated API with table name validation
const response = await fetch('/api/admin/query', { const response = await fetch('/api/admin/table-data', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({ tableName }),
query: `SELECT * FROM "${tableName}" LIMIT 100`,
}),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Query failed'); const data = await response.json();
throw new Error(data.error || 'Query failed');
} }
const data = await response.json(); const data = await response.json();

View File

@@ -0,0 +1,60 @@
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
import { db } from '@/utils/db';
import { getSession } from '@/utils/session';
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName } = await request.json();
if (!tableName) {
return NextResponse.json(
{ error: 'Table name is required' },
{ status: 400 },
);
}
// Validate table name against schema to prevent SQL injection
const tablesResult = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`);
if (tablesResult.rows.length === 0) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
// Table name is validated, sanitize and use
const safeTableName = String(tableName).replace(/\W/g, '');
const result = await db.execute(sql.raw(`SELECT * FROM "${safeTableName}" LIMIT 100`));
return NextResponse.json({
rows: result.rows,
rowCount: result.rowCount,
fields: result.fields?.map(field => ({
name: field.name,
dataTypeID: field.dataTypeID,
})) || [],
});
} catch (error: any) {
console.error('Table query error:', error);
return NextResponse.json(
{ error: error.message || 'Query failed' },
{ status: 500 },
);
}
}

View File

@@ -3,16 +3,12 @@ import { cookies } from 'next/headers';
const SESSION_COOKIE_NAME = 'admin-session'; const SESSION_COOKIE_NAME = 'admin-session';
// Get JWT secret and throw error if not provided in production // Get JWT secret and throw error if not provided
function getJwtSecret(): Uint8Array { function getJwtSecret(): Uint8Array {
const secret = process.env.JWT_SECRET; const secret = process.env.JWT_SECRET;
if (!secret) { if (!secret) {
if (process.env.NODE_ENV === 'production') { throw new Error('JWT_SECRET environment variable is required');
throw new Error('JWT_SECRET environment variable is required in production');
}
console.warn('JWT_SECRET not set, using development default');
return new TextEncoder().encode('development-secret-change-in-production');
} }
return new TextEncoder().encode(secret); return new TextEncoder().encode(secret);