mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +00:00
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:
4
.env
4
.env
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
60
src/app/api/admin/table-data/route.ts
Normal file
60
src/app/api/admin/table-data/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user