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);