From b299a0b6024518fcb35aacccb01643dcd86b2745 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 18 May 2026 13:03:49 +0200 Subject: [PATCH] fix: validate plugin/theme id in marketplace install to block path traversal --- app/api/admin/marketplace/route.ts | 16 ++++++++++++++-- lib/admin/plugin-registry.ts | 8 ++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/api/admin/marketplace/route.ts b/app/api/admin/marketplace/route.ts index feb649ac..5251d812 100644 --- a/app/api/admin/marketplace/route.ts +++ b/app/api/admin/marketplace/route.ts @@ -164,6 +164,18 @@ export async function POST(request: NextRequest) { const now = new Date().toISOString(); + // Resolve and strictly validate the id used as a filename. Marketplace + // bundles are authored by a third-party publisher; without this an id + // like "../../foo" causes savePlugin/saveTheme to write outside the + // plugins/themes dir via path.join. + const resolvedId = typeof manifest.id === 'string' && manifest.id ? manifest.id : slug; + if (typeof resolvedId !== 'string' || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(resolvedId)) { + return NextResponse.json( + { error: 'Invalid id: must be lowercase alphanumeric with hyphens, min 2 chars' }, + { status: 400 }, + ); + } + if (type === 'theme') { // Read theme.css const cssFile = zip.file(root + 'theme.css'); @@ -183,7 +195,7 @@ export async function POST(request: NextRequest) { } const theme: ServerTheme = { - id: (manifest.id as string) || slug, + id: resolvedId, name: (manifest.name as string) || slug, version: (manifest.version as string) || version, author: (manifest.author as string) || 'Unknown', @@ -280,7 +292,7 @@ export async function POST(request: NextRequest) { } const plugin: ServerPlugin = { - id: (manifest.id as string) || slug, + id: resolvedId, name: (manifest.name as string) || slug, version: (manifest.version as string) || version, author: (manifest.author as string) || 'Unknown', diff --git a/lib/admin/plugin-registry.ts b/lib/admin/plugin-registry.ts index be4b6b26..1d914b0e 100644 --- a/lib/admin/plugin-registry.ts +++ b/lib/admin/plugin-registry.ts @@ -139,11 +139,16 @@ export async function getPlugin(id: string): Promise { return registry.plugins.find(p => p.id === id) || null; } +const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; + export async function savePlugin( plugin: ServerPlugin, code: string, ): Promise { assertWritable('install plugin'); + if (!SAFE_ID_RE.test(plugin.id)) { + throw new Error('Invalid plugin id'); + } const dir = getPluginsDir(); await ensureDir(dir); @@ -227,6 +232,9 @@ export async function saveTheme( css: string, ): Promise { assertWritable('install theme'); + if (!SAFE_ID_RE.test(theme.id)) { + throw new Error('Invalid theme id'); + } const dir = getThemesDir(); await ensureDir(dir);