fix(security): harden input validation schemas and fix SSR sanitize bypass
- blueprint rolePresets: cap array at 100 items to prevent storage abuse - notification CreateManagedNotification: add .max() on title (500), body (2000), type (100), entityType/entityId (200), link (1000), taskAction (200) - settings: add .max() on all string config fields; add regex allowlist (/^[a-zA-Z0-9._-]+$/) on model name fields (geminiModel, azureDalleDeployment, azureOpenAiDeployment) to prevent path manipulation - sanitizeHtml: fix SSR bypass — server-side branch now strips HTML tags instead of returning the raw string unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,9 @@ import DOMPurify from "dompurify";
|
|||||||
* SSR-safe: returns the input unchanged on the server.
|
* SSR-safe: returns the input unchanged on the server.
|
||||||
*/
|
*/
|
||||||
export function sanitizeHtml(dirty: string): string {
|
export function sanitizeHtml(dirty: string): string {
|
||||||
if (typeof window === "undefined") return dirty;
|
if (typeof window === "undefined") {
|
||||||
|
// Server-side: strip all HTML tags as a safe fallback
|
||||||
|
return dirty.replace(/<[^>]*>/g, "");
|
||||||
|
}
|
||||||
return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] });
|
return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const blueprintUpdateInputSchema = z.object({
|
|||||||
|
|
||||||
export const blueprintRolePresetsInputSchema = z.object({
|
export const blueprintRolePresetsInputSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
rolePresets: z.array(z.unknown()),
|
rolePresets: z.array(z.unknown()).max(100),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const blueprintBatchDeleteInputSchema = z.object({
|
export const blueprintBatchDeleteInputSchema = z.object({
|
||||||
|
|||||||
@@ -180,16 +180,16 @@ export const MarkNotificationReadInputSchema = z.object({
|
|||||||
|
|
||||||
export const CreateManagedNotificationInputSchema = z.object({
|
export const CreateManagedNotificationInputSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
type: z.string(),
|
type: z.string().max(100),
|
||||||
title: z.string(),
|
title: z.string().max(500),
|
||||||
body: z.string().optional(),
|
body: z.string().max(2000).optional(),
|
||||||
entityId: z.string().optional(),
|
entityId: z.string().max(200).optional(),
|
||||||
entityType: z.string().optional(),
|
entityType: z.string().max(200).optional(),
|
||||||
category: categoryEnum.optional(),
|
category: categoryEnum.optional(),
|
||||||
priority: priorityEnum.optional(),
|
priority: priorityEnum.optional(),
|
||||||
link: z.string().optional(),
|
link: z.string().max(1000).optional(),
|
||||||
taskStatus: taskStatusEnum.optional(),
|
taskStatus: taskStatusEnum.optional(),
|
||||||
taskAction: z.string().optional(),
|
taskAction: z.string().max(200).optional(),
|
||||||
assigneeId: z.string().optional(),
|
assigneeId: z.string().optional(),
|
||||||
dueDate: z.date().optional(),
|
dueDate: z.date().optional(),
|
||||||
channel: channelEnum.optional(),
|
channel: channelEnum.optional(),
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ const SENSITIVE_FIELDS = new Set([
|
|||||||
export const settingsUpdateInputSchema = z.object({
|
export const settingsUpdateInputSchema = z.object({
|
||||||
aiProvider: z.enum(["openai", "azure"]).optional(),
|
aiProvider: z.enum(["openai", "azure"]).optional(),
|
||||||
azureOpenAiEndpoint: z.string().url().optional().or(z.literal("")),
|
azureOpenAiEndpoint: z.string().url().optional().or(z.literal("")),
|
||||||
azureOpenAiDeployment: z.string().optional(),
|
azureOpenAiDeployment: z.string().regex(/^[a-zA-Z0-9._-]+$/).max(200).optional(),
|
||||||
azureOpenAiApiKey: z.string().optional(),
|
azureOpenAiApiKey: z.string().max(500).optional(),
|
||||||
azureApiVersion: z.string().optional(),
|
azureApiVersion: z.string().max(500).optional(),
|
||||||
aiMaxCompletionTokens: z.number().int().min(50).max(4000).optional(),
|
aiMaxCompletionTokens: z.number().int().min(50).max(4000).optional(),
|
||||||
aiTemperature: z.number().min(0).max(2).optional(),
|
aiTemperature: z.number().min(0).max(2).optional(),
|
||||||
aiSummaryPrompt: z.string().optional(),
|
aiSummaryPrompt: z.string().max(4000).optional(),
|
||||||
scoreWeights: z.object({
|
scoreWeights: z.object({
|
||||||
skillDepth: z.number().min(0).max(1),
|
skillDepth: z.number().min(0).max(1),
|
||||||
skillBreadth: z.number().min(0).max(1),
|
skillBreadth: z.number().min(0).max(1),
|
||||||
@@ -47,21 +47,21 @@ export const settingsUpdateInputSchema = z.object({
|
|||||||
{ message: "Score weights must sum to 1.0" },
|
{ message: "Score weights must sum to 1.0" },
|
||||||
).optional(),
|
).optional(),
|
||||||
scoreVisibleRoles: z.array(z.enum(["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"])).optional(),
|
scoreVisibleRoles: z.array(z.enum(["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"])).optional(),
|
||||||
smtpHost: z.string().optional(),
|
smtpHost: z.string().max(500).optional(),
|
||||||
smtpPort: z.number().int().min(1).max(65535).optional(),
|
smtpPort: z.number().int().min(1).max(65535).optional(),
|
||||||
smtpUser: z.string().optional(),
|
smtpUser: z.string().max(500).optional(),
|
||||||
smtpPassword: z.string().optional(),
|
smtpPassword: z.string().max(500).optional(),
|
||||||
smtpFrom: z.string().email().optional().or(z.literal("")),
|
smtpFrom: z.string().email().optional().or(z.literal("")),
|
||||||
smtpTls: z.boolean().optional(),
|
smtpTls: z.boolean().optional(),
|
||||||
anonymizationEnabled: z.boolean().optional(),
|
anonymizationEnabled: z.boolean().optional(),
|
||||||
anonymizationDomain: z.string().trim().min(1).optional(),
|
anonymizationDomain: z.string().trim().min(1).max(500).optional(),
|
||||||
anonymizationSeed: z.string().trim().min(1).optional().or(z.literal("")),
|
anonymizationSeed: z.string().trim().min(1).max(500).optional().or(z.literal("")),
|
||||||
anonymizationMode: z.enum(["global"]).optional(),
|
anonymizationMode: z.enum(["global"]).optional(),
|
||||||
azureDalleDeployment: z.string().optional(),
|
azureDalleDeployment: z.string().regex(/^[a-zA-Z0-9._-]+$/).max(200).optional(),
|
||||||
azureDalleEndpoint: z.string().url().optional().or(z.literal("")),
|
azureDalleEndpoint: z.string().url().optional().or(z.literal("")),
|
||||||
azureDalleApiKey: z.string().optional(),
|
azureDalleApiKey: z.string().max(500).optional(),
|
||||||
geminiApiKey: z.string().optional(),
|
geminiApiKey: z.string().max(500).optional(),
|
||||||
geminiModel: z.string().optional(),
|
geminiModel: z.string().regex(/^[a-zA-Z0-9._-]+$/).max(200).optional(),
|
||||||
imageProvider: z.enum(["dalle", "gemini"]).optional(),
|
imageProvider: z.enum(["dalle", "gemini"]).optional(),
|
||||||
vacationDefaultDays: z.number().int().min(0).max(365).optional(),
|
vacationDefaultDays: z.number().int().min(0).max(365).optional(),
|
||||||
timelineUndoMaxSteps: z.number().int().min(1).max(200).optional(),
|
timelineUndoMaxSteps: z.number().int().min(1).max(200).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user