changes made to sanitize html to overcome the VAPT alets

This commit is contained in:
laxmanhalaki 2026-02-09 11:22:40 +05:30
parent 81565d294b
commit d285ea88d8
8 changed files with 786 additions and 756 deletions

View File

@ -1,5 +1,6 @@
import * as React from "react";
import { cn } from "@/components/ui/utils";
import { sanitizeHTML } from "@/utils/sanitizer";
interface FormattedDescriptionProps {
content: string;
@ -33,7 +34,8 @@ export function FormattedDescription({ content, className }: FormattedDescriptio
return `<div class="table-wrapper">${match}</div>`;
});
return processed;
// Sanitize the content to prevent CSP violations (onclick, style tags, etc.)
return sanitizeHTML(processed);
}, [content]);
if (!content) return null;

View File

@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
import { sanitizeHTML } from '../../utils/sanitizer';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Avatar, AvatarFallback } from '../ui/avatar';
@ -166,7 +167,8 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)
const formatMessage = (content: string) => {
// Simple mention highlighting
return content.replace(/@(\w+\s?\w+)/g, '<span class="text-blue-600 font-medium">@$1</span>');
const formatted = content.replace(/@(\w+\s?\w+)/g, '<span class="text-blue-600 font-medium">@$1</span>');
return sanitizeHTML(formatted);
};
return (
@ -195,11 +197,10 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)
<div key={msg.id} className={`flex gap-3 ${msg.isSystem ? 'justify-center' : ''}`}>
{!msg.isSystem && (
<Avatar className="h-8 w-8 flex-shrink-0">
<AvatarFallback className={`text-white text-xs ${
msg.user.role === 'Initiator' ? 'bg-re-green' :
<AvatarFallback className={`text-white text-xs ${msg.user.role === 'Initiator' ? 'bg-re-green' :
msg.user.role === 'Current User' ? 'bg-blue-500' :
'bg-re-light-green'
}`}>
'bg-re-light-green'
}`}>
{msg.user.avatar}
</AvatarFallback>
</Avatar>
@ -306,9 +307,8 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)
<div key={index} className="flex items-center gap-3">
<div className="relative">
<Avatar className="h-8 w-8">
<AvatarFallback className={`text-white text-xs ${
participant.role === 'Initiator' ? 'bg-re-green' : 'bg-re-light-green'
}`}>
<AvatarFallback className={`text-white text-xs ${participant.role === 'Initiator' ? 'bg-re-green' : 'bg-re-light-green'
}`}>
{participant.avatar}
</AvatarFallback>
</Avatar>

View File

@ -54,13 +54,13 @@ function ChartContainer({
<div
data-slot="chart"
data-chart={chartId}
style={getChartStyle(config)}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
@ -69,37 +69,39 @@ function ChartContainer({
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const getChartStyle = (config: ChartConfig) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
return {};
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
const styles: Record<string, string> = {};
colorConfig.forEach(([key, itemConfig]) => {
// For simplicity, we'll use the default color or the light theme color
// If you need per-theme variables, they should be handled via CSS classes or media queries
// but applying them here as inline styles is CSP-safe.
const color = itemConfig.color || itemConfig.theme?.light;
if (color) {
styles[`--color-${key}`] = color;
}
// Handle dark theme if present
const darkColor = itemConfig.theme?.dark;
if (darkColor) {
styles[`--color-${key}-dark`] = darkColor;
}
});
return styles as React.CSSProperties;
};
// Deprecated: Kept for backward compatibility if needed in other files.
const ChartStyle = () => {
return null;
};
const ChartTooltip = RechartsPrimitive.Tooltip;
@ -316,8 +318,8 @@ function getPayloadConfigFromPayload(
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;

View File

@ -3,6 +3,7 @@ import { cn } from "./utils";
import { Button } from "./button";
import { Bold, Italic, Underline, List, ListOrdered, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight, Highlighter, X, Type } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { sanitizeHTML } from "@/utils/sanitizer";
interface RichTextEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
value: string;
@ -59,7 +60,8 @@ export function RichTextEditor({
// Only update if the value actually changed externally
const currentValue = editorRef.current.innerHTML;
if (currentValue !== value) {
editorRef.current.innerHTML = value || '';
// Sanitize incoming content
editorRef.current.innerHTML = sanitizeHTML(value || '');
}
}
}, [value]);
@ -230,9 +232,9 @@ export function RichTextEditor({
selection.removeAllRanges();
selection.addRange(range);
// Trigger onChange
// Trigger onChange with sanitized content
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
}, [onChange, cleanWordHTML]);
@ -377,7 +379,7 @@ export function RichTextEditor({
// Update content
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
// Check active formats after a short delay
@ -529,7 +531,7 @@ export function RichTextEditor({
// Update content
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
// Close popover
@ -633,7 +635,7 @@ export function RichTextEditor({
// Update content
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
// Close popover
@ -646,7 +648,7 @@ export function RichTextEditor({
// Handle input changes
const handleInput = React.useCallback(() => {
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
checkActiveFormats();
}, [onChange, checkActiveFormats]);
@ -682,7 +684,7 @@ export function RichTextEditor({
const handleBlur = React.useCallback(() => {
setIsFocused(false);
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
onChange(sanitizeHTML(editorRef.current.innerHTML));
}
}, [onChange]);

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import { useState, useRef, useEffect } from 'react';
import { getWorkNotes, createWorkNoteMultipart, getWorkflowDetails, downloadWorkNoteAttachment, getWorkNoteAttachmentPreviewUrl } from '@/services/workflowApi';
import { sanitizeHTML } from '@/utils/sanitizer';
import { toast } from 'sonner';
import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
import { formatDateTime } from '@/utils/dateFormatter';
@ -58,9 +59,11 @@ interface WorkNoteChatSimpleProps {
}
const formatMessage = (content: string) => {
return content
const formattedContent = content
.replace(/@([\w\s]+)(?=\s|$|[.,!?])/g, '<span class="inline-flex items-center px-2 py-1 rounded-md bg-blue-100 text-blue-800 font-medium text-sm">@$1</span>')
.replace(/\n/g, '<br />');
return sanitizeHTML(formattedContent);
};
const FileIcon = ({ type }: { type: string }) => {
@ -140,7 +143,7 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
if (details?.workflow?.requestId) {
joinedId = details.workflow.requestId;
}
} catch {}
} catch { }
try {
// Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL)
const s = getSocket(); // Uses getSocketBaseUrl() helper internally
@ -189,7 +192,7 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
}
})();
return () => {
try { (window as any).__wn_cleanup?.(); } catch {}
try { (window as any).__wn_cleanup?.(); } catch { }
};
}, [requestId, currentUserId]);
@ -218,7 +221,7 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
};
}) : [];
setMessages(mapped as any);
} catch {}
} catch { }
}
setMessage('');
setSelectedFiles([]);
@ -257,7 +260,7 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
} as any;
});
setMessages(mapped);
} catch {}
} catch { }
} else {
(async () => {
try {
@ -394,11 +397,10 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
<div key={msg.id} className={`flex gap-3 ${msg.isSystem ? 'justify-center' : isCurrentUser ? 'justify-end' : ''}`}>
{!msg.isSystem && !isCurrentUser && (
<Avatar className="h-10 w-10 flex-shrink-0 ring-2 ring-white shadow-sm">
<AvatarFallback className={`text-white font-semibold text-sm ${
msg.user.role === 'Initiator' ? 'bg-green-600' :
msg.user.role === 'Approver' ? 'bg-blue-600' :
'bg-slate-600'
}`}>
<AvatarFallback className={`text-white font-semibold text-sm ${msg.user.role === 'Initiator' ? 'bg-green-600' :
msg.user.role === 'Approver' ? 'bg-blue-600' :
'bg-slate-600'
}`}>
{msg.user.avatar}
</AvatarFallback>
</Avatar>
@ -451,70 +453,70 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
const attachmentId = attachment.attachmentId || attachment.attachment_id;
return (
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors">
<div className="flex-shrink-0">
<FileIcon type={fileType} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-700 truncate">
{fileName}
</p>
{fileSize && (
<p className="text-xs text-gray-500">
{formatFileSize(fileSize)}
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors">
<div className="flex-shrink-0">
<FileIcon type={fileType} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-700 truncate">
{fileName}
</p>
)}
</div>
{fileSize && (
<p className="text-xs text-gray-500">
{formatFileSize(fileSize)}
</p>
)}
</div>
{/* Preview button for images and PDFs */}
{attachmentId && (fileType.includes('image') || fileType.includes('pdf')) && (
{/* Preview button for images and PDFs */}
{attachmentId && (fileType.includes('image') || fileType.includes('pdf')) && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 flex-shrink-0 hover:bg-purple-100 hover:text-purple-600"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const previewUrl = getWorkNoteAttachmentPreviewUrl(attachmentId);
setPreviewFile({
fileName,
fileType,
fileUrl: previewUrl,
fileSize,
attachmentId
});
}}
title="Preview file"
>
<Eye className="w-4 h-4" />
</Button>
)}
{/* Download button */}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 flex-shrink-0 hover:bg-purple-100 hover:text-purple-600"
onClick={(e) => {
className="h-8 w-8 p-0 flex-shrink-0 hover:bg-blue-100 hover:text-blue-600"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const previewUrl = getWorkNoteAttachmentPreviewUrl(attachmentId);
setPreviewFile({
fileName,
fileType,
fileUrl: previewUrl,
fileSize,
attachmentId
});
if (!attachmentId) {
toast.error('Cannot download: Attachment ID missing');
return;
}
try {
await downloadWorkNoteAttachment(attachmentId);
} catch (error) {
toast.error('Failed to download file');
}
}}
title="Preview file"
title="Download file"
>
<Eye className="w-4 h-4" />
<Download className="w-4 h-4" />
</Button>
)}
{/* Download button */}
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 flex-shrink-0 hover:bg-blue-100 hover:text-blue-600"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (!attachmentId) {
toast.error('Cannot download: Attachment ID missing');
return;
}
try {
await downloadWorkNoteAttachment(attachmentId);
} catch (error) {
toast.error('Failed to download file');
}
}}
title="Download file"
>
<Download className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
</div>
@ -528,11 +530,10 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
<button
key={index}
onClick={() => addReaction(msg.id, reaction.emoji)}
className={`flex items-center gap-1 px-2 py-1 rounded-full text-sm transition-colors flex-shrink-0 ${
reaction.users.includes('You')
className={`flex items-center gap-1 px-2 py-1 rounded-full text-sm transition-colors flex-shrink-0 ${reaction.users.includes('You')
? 'bg-blue-100 text-blue-800 border border-blue-200'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
}`}
>
<span>{reaction.emoji}</span>
<span className="text-xs font-medium">{reaction.users.length}</span>

View File

@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { FileText, AlertCircle } from 'lucide-react';
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
import { sanitizeHTML } from '@/utils/sanitizer';
interface AdminRequestReviewStepProps {
template: RequestTemplate;
@ -47,7 +48,7 @@ export function AdminRequestReviewStep({
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Request Detail</span>
<div
className="text-sm text-gray-700 mt-1 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: formData.description }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(formData.description) }}
/>
</div>

25
src/utils/sanitizer.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* Sanitizes HTML content by removing dangerous attributes and tags.
* This is used to comply with CSP policies and prevent XSS.
*/
export function sanitizeHTML(html: string): string {
if (!html) return '';
// 1. Remove script tags completely
let sanitized = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
// 2. Remove all "on*" event handler attributes (onclick, onload, etc.)
// This handles attributes like onclick="alert(1)" or onclick='alert(1)' or onclick=alert(1)
sanitized = sanitized.replace(/\s+on\w+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, '');
// 3. Remove "javascript:" pseudo-protocols in href or src
sanitized = sanitized.replace(/(href|src)\s*=\s*(?:'javascript:[^']*'|"javascript:[^"]*"|javascript:[^\s>]+)/gi, '$1="#"');
// 4. Remove <style> tags (to comply with style-src)
sanitized = sanitized.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
// 5. Remove meta and link tags (except for purely visual ones if needed, but safer to remove)
sanitized = sanitized.replace(/<(meta|link|iframe|object|embed|applet)[^>]*>/gi, '');
return sanitized;
}