fix: validate plugin/theme id in marketplace install to block path traversal

This commit is contained in:
Linus Rath
2026-05-18 13:03:49 +02:00
parent f275fbe2e4
commit b299a0b602
2 changed files with 22 additions and 2 deletions
+14 -2
View File
@@ -164,6 +164,18 @@ export async function POST(request: NextRequest) {
const now = new Date().toISOString(); 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') { if (type === 'theme') {
// Read theme.css // Read theme.css
const cssFile = zip.file(root + 'theme.css'); const cssFile = zip.file(root + 'theme.css');
@@ -183,7 +195,7 @@ export async function POST(request: NextRequest) {
} }
const theme: ServerTheme = { const theme: ServerTheme = {
id: (manifest.id as string) || slug, id: resolvedId,
name: (manifest.name as string) || slug, name: (manifest.name as string) || slug,
version: (manifest.version as string) || version, version: (manifest.version as string) || version,
author: (manifest.author as string) || 'Unknown', author: (manifest.author as string) || 'Unknown',
@@ -280,7 +292,7 @@ export async function POST(request: NextRequest) {
} }
const plugin: ServerPlugin = { const plugin: ServerPlugin = {
id: (manifest.id as string) || slug, id: resolvedId,
name: (manifest.name as string) || slug, name: (manifest.name as string) || slug,
version: (manifest.version as string) || version, version: (manifest.version as string) || version,
author: (manifest.author as string) || 'Unknown', author: (manifest.author as string) || 'Unknown',
+8
View File
@@ -139,11 +139,16 @@ export async function getPlugin(id: string): Promise<ServerPlugin | null> {
return registry.plugins.find(p => p.id === id) || null; 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( export async function savePlugin(
plugin: ServerPlugin, plugin: ServerPlugin,
code: string, code: string,
): Promise<void> { ): Promise<void> {
assertWritable('install plugin'); assertWritable('install plugin');
if (!SAFE_ID_RE.test(plugin.id)) {
throw new Error('Invalid plugin id');
}
const dir = getPluginsDir(); const dir = getPluginsDir();
await ensureDir(dir); await ensureDir(dir);
@@ -227,6 +232,9 @@ export async function saveTheme(
css: string, css: string,
): Promise<void> { ): Promise<void> {
assertWritable('install theme'); assertWritable('install theme');
if (!SAFE_ID_RE.test(theme.id)) {
throw new Error('Invalid theme id');
}
const dir = getThemesDir(); const dir = getThemesDir();
await ensureDir(dir); await ensureDir(dir);