fix: harden deploys and close top security holes after /events outage
Isolate next dev from production .next, add build-guard/atomic deploy/health watchdog, error boundaries, and fix JWT startup, meetup leaks, media path traversal, SVG/memory uploads, and JSON-LD escaping. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -180,50 +180,72 @@ function handleVideoStream(
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// Media blobs are stored as ULIDs (Crockford base32, 26 chars). Reject anything
|
||||
// else before joining into the storage path so `../` cannot escape the root.
|
||||
const MEDIA_ID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
||||
|
||||
function resolveSafeMediaPath(root: string, id: string): string | null {
|
||||
if (!MEDIA_ID_RE.test(id)) return null;
|
||||
const resolvedRoot = path.resolve(root);
|
||||
const filePath = path.resolve(resolvedRoot, id);
|
||||
const rootPrefix = resolvedRoot.endsWith(path.sep)
|
||||
? resolvedRoot
|
||||
: resolvedRoot + path.sep;
|
||||
if (filePath !== resolvedRoot && !filePath.startsWith(rootPrefix)) {
|
||||
return null;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> | { id: string } }
|
||||
) {
|
||||
const params = await Promise.resolve(context.params);
|
||||
const id = params.id;
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const root = getMediaStorageRoot();
|
||||
const filePath = path.join(root, id);
|
||||
if (!fileExists(filePath)) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const meta = readMeta(root, id);
|
||||
if (!meta) {
|
||||
return NextResponse.json({ error: 'Metadata not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const widthParam = searchParams.get('w');
|
||||
|
||||
if (meta.type === 'image' && widthParam) {
|
||||
const width = parseInt(widthParam, 10);
|
||||
if (isNaN(width) || width < 1 || width > 4096) {
|
||||
return NextResponse.json({ error: 'Invalid width' }, { status: 400 });
|
||||
try {
|
||||
const params = await Promise.resolve(context.params);
|
||||
const id = params.id;
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
return handleImageResize(root, filePath, width, meta, id);
|
||||
}
|
||||
|
||||
if (meta.type === 'video') {
|
||||
const rangeHeader = request.headers.get('range');
|
||||
return handleVideoStream(filePath, meta, rangeHeader);
|
||||
}
|
||||
const root = getMediaStorageRoot();
|
||||
const filePath = resolveSafeMediaPath(root, id);
|
||||
if (!filePath || !fileExists(filePath)) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return new NextResponse(new Uint8Array(buffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': meta.mimeType,
|
||||
'Content-Length': String(buffer.length),
|
||||
...CACHE_HEADERS,
|
||||
},
|
||||
});
|
||||
const meta = readMeta(root, id);
|
||||
if (!meta) {
|
||||
return NextResponse.json({ error: 'Metadata not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const widthParam = searchParams.get('w');
|
||||
|
||||
if (meta.type === 'image' && widthParam) {
|
||||
const width = parseInt(widthParam, 10);
|
||||
if (isNaN(width) || width < 1 || width > 4096) {
|
||||
return NextResponse.json({ error: 'Invalid width' }, { status: 400 });
|
||||
}
|
||||
return await handleImageResize(root, filePath, width, meta, id);
|
||||
}
|
||||
|
||||
if (meta.type === 'video') {
|
||||
const rangeHeader = request.headers.get('range');
|
||||
return handleVideoStream(filePath, meta, rangeHeader);
|
||||
}
|
||||
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return new NextResponse(new Uint8Array(buffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': meta.mimeType,
|
||||
'Content-Length': String(buffer.length),
|
||||
...CACHE_HEADERS,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Media serve error:', err);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user