fix: address all final review findings for Phase 3
- CRITICAL: Fix command injection in discoveryService (execFile instead of exec, path validation regex) - CRITICAL: Add Zod validation on discover API endpoint - HIGH: Add Zod validation on discover/approve endpoint - HIGH: Add array length limits to import schema (1000/100/100) - HIGH: Fix theme broadcast echo loop (setTimeout vs queueMicrotask) - MEDIUM: Singleton BroadcastChannel instead of create-per-send - MEDIUM: Exclude sensitive APIs from service worker cache - MEDIUM: Fix TypeScript cast errors in exportService tests
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAdmin } from '$lib/server/middleware/authorize.js';
|
||||
import { discoverAll, type DiscoveryConfig } from '$lib/server/services/discoveryService.js';
|
||||
import { success, error } from '$lib/server/utils/response.js';
|
||||
|
||||
const discoverConfigSchema = z.object({
|
||||
dockerSocketPath: z.string().regex(/^[\w/.:-]+$/).optional(),
|
||||
traefikApiUrl: z.string().url().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/discover — Scan Docker and Traefik for services. Admin only.
|
||||
*
|
||||
@@ -12,16 +18,21 @@ import { success, error } from '$lib/server/utils/response.js';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
requireAdmin(event);
|
||||
|
||||
let body: DiscoveryConfig;
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
body = await event.request.json();
|
||||
rawBody = await event.request.json();
|
||||
} catch {
|
||||
return json(error('Invalid JSON body'), { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = discoverConfigSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return json(error(`Validation failed: ${parsed.error.issues.map((i) => i.message).join(', ')}`), { status: 400 });
|
||||
}
|
||||
|
||||
const config: DiscoveryConfig = {
|
||||
dockerSocketPath: body.dockerSocketPath || undefined,
|
||||
traefikApiUrl: body.traefikApiUrl || undefined
|
||||
dockerSocketPath: parsed.data.dockerSocketPath || undefined,
|
||||
traefikApiUrl: parsed.data.traefikApiUrl || undefined
|
||||
};
|
||||
|
||||
if (!config.dockerSocketPath && !config.traefikApiUrl) {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAdmin } from '$lib/server/middleware/authorize.js';
|
||||
import { create } from '$lib/server/services/appService.js';
|
||||
import { success, error } from '$lib/server/utils/response.js';
|
||||
|
||||
interface ApproveServiceInput {
|
||||
readonly name: string;
|
||||
readonly url: string;
|
||||
readonly source: 'docker' | 'traefik';
|
||||
readonly icon?: string;
|
||||
readonly description?: string;
|
||||
}
|
||||
|
||||
interface ApproveBody {
|
||||
readonly services: readonly ApproveServiceInput[];
|
||||
}
|
||||
const approveSchema = z.object({
|
||||
services: z.array(z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
source: z.enum(['docker', 'traefik']),
|
||||
icon: z.string().optional(),
|
||||
description: z.string().optional()
|
||||
})).min(1).max(100)
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/discover/approve — Approve discovered services and create app entries. Admin only.
|
||||
@@ -24,26 +23,24 @@ interface ApproveBody {
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const user = requireAdmin(event);
|
||||
|
||||
let body: ApproveBody;
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
body = await event.request.json();
|
||||
rawBody = await event.request.json();
|
||||
} catch {
|
||||
return json(error('Invalid JSON body'), { status: 400 });
|
||||
}
|
||||
|
||||
if (!body.services || !Array.isArray(body.services) || body.services.length === 0) {
|
||||
return json(error('At least one service must be provided for approval'), { status: 400 });
|
||||
const parsed = approveSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return json(error(`Validation failed: ${parsed.error.issues.map((i) => i.message).join(', ')}`), { status: 400 });
|
||||
}
|
||||
|
||||
const body = parsed.data;
|
||||
|
||||
const created: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const service of body.services) {
|
||||
if (!service.name || !service.url) {
|
||||
errors.push(`Skipped invalid service entry (missing name or url)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const app = await create({
|
||||
name: service.name,
|
||||
|
||||
Reference in New Issue
Block a user