changes made to sanitize html to overcome the VAPT alets
This commit is contained in:
parent
81565d294b
commit
d285ea88d8
@ -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;
|
||||
|
||||
@ -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,8 +197,7 @@ 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'
|
||||
}`}>
|
||||
@ -306,8 +307,7 @@ 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>
|
||||
|
||||
@ -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")}
|
||||
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;
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { getWorkNotes, createWorkNoteMultipart, getWorkflowDetails, downloadWorkNoteAttachment, getWorkNoteAttachmentPreviewUrl, addSpectator, addApproverAtLevel } from '@/services/workflowApi';
|
||||
import { sanitizeHTML } from '@/utils/sanitizer';
|
||||
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
||||
import { toast } from 'sonner';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
@ -109,9 +110,7 @@ const getStatusText = (status: string) => {
|
||||
|
||||
const formatMessage = (content: string) => {
|
||||
// Enhanced mention highlighting - Blue color with extra bold font for high visibility
|
||||
// Matches: @username or @FirstName LastName (only one space allowed for first name + last name)
|
||||
// Pattern: @word or @word word (stops after second word)
|
||||
return content
|
||||
const formattedContent = content
|
||||
.replace(/@(\w+(?:\s+\w+)?)(?=\s|$|[.,!?;:]|@)/g, (match, mention, offset, string) => {
|
||||
const afterPos = offset + match.length;
|
||||
const afterChar = string[afterPos];
|
||||
@ -124,6 +123,8 @@ const formatMessage = (content: string) => {
|
||||
return match;
|
||||
})
|
||||
.replace(/\n/g, '<br />');
|
||||
|
||||
return sanitizeHTML(formattedContent);
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
@ -1274,8 +1275,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
|
||||
<div key={msg.id} className={`flex gap-2 sm:gap-3 lg:gap-4 ${msg.isSystem ? 'justify-center' : isCurrentUser ? 'justify-end' : ''}`}>
|
||||
{!msg.isSystem && !isCurrentUser && (
|
||||
<Avatar className="h-8 w-8 sm:h-10 sm:w-10 lg:h-12 lg:w-12 flex-shrink-0 ring-1 sm:ring-2 ring-white shadow-sm">
|
||||
<AvatarFallback className={`text-white font-semibold text-xs sm:text-sm ${
|
||||
msg.user.role === 'Initiator' ? 'bg-green-600' :
|
||||
<AvatarFallback className={`text-white font-semibold text-xs sm:text-sm ${msg.user.role === 'Initiator' ? 'bg-green-600' :
|
||||
msg.user.role === 'Current User' ? 'bg-blue-500' :
|
||||
msg.user.role === 'System' ? 'bg-gray-500' :
|
||||
'bg-slate-600'
|
||||
@ -1414,8 +1414,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => addReaction(msg.id, reaction.emoji)}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs sm:text-sm transition-colors flex-shrink-0 ${
|
||||
reaction.users.includes('You')
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs sm: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'
|
||||
}`}
|
||||
@ -1564,8 +1563,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
|
||||
className="w-full flex items-center gap-3 p-3 hover:bg-blue-50 rounded-lg text-left transition-colors border border-transparent hover:border-blue-200"
|
||||
>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarFallback className={`text-white text-sm font-semibold ${
|
||||
participant.role === 'Initiator' ? 'bg-green-600' :
|
||||
<AvatarFallback className={`text-white text-sm font-semibold ${participant.role === 'Initiator' ? 'bg-green-600' :
|
||||
participant.role === 'Approver' ? 'bg-purple-600' :
|
||||
'bg-blue-500'
|
||||
}`}>
|
||||
@ -1713,8 +1711,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Avatar className="h-9 w-9 sm:h-10 sm:w-10">
|
||||
<AvatarFallback className={`text-white font-semibold text-sm ${
|
||||
participant.role === 'Initiator' ? 'bg-green-600' :
|
||||
<AvatarFallback className={`text-white font-semibold text-sm ${participant.role === 'Initiator' ? 'bg-green-600' :
|
||||
isCurrentUser ? 'bg-blue-500' : 'bg-slate-600'
|
||||
}`}>
|
||||
{participant.avatar}
|
||||
|
||||
@ -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 }) => {
|
||||
@ -394,8 +397,7 @@ 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' :
|
||||
<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'
|
||||
}`}>
|
||||
@ -528,8 +530,7 @@ 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'
|
||||
}`}
|
||||
|
||||
@ -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
25
src/utils/sanitizer.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user