fix: validate plugin/theme id in marketplace install to block path traversal
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user