security: fix XSS vulnerabilities in GanttChart and AuditPage
- Add escapeHtml utility function for HTML entity encoding - Apply escapeHtml to GanttChart popup HTML template - Apply escapeHtml to AuditPage PDF export HTML template This prevents potential XSS attacks if task names, user names, or other dynamic content contains malicious HTML/JavaScript. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import Gantt, { GanttTask, ViewMode } from 'frappe-gantt'
|
|||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies'
|
import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies'
|
||||||
import { CircularDependencyError, parseCircularError } from './CircularDependencyError'
|
import { CircularDependencyError, parseCircularError } from './CircularDependencyError'
|
||||||
|
import { escapeHtml } from '../utils/escapeHtml'
|
||||||
|
|
||||||
interface CycleDetails {
|
interface CycleDetails {
|
||||||
cycle: string[]
|
cycle: string[]
|
||||||
@@ -198,13 +199,14 @@ export function GanttChart({
|
|||||||
const originalTask = taskMap.current.get(task.id)
|
const originalTask = taskMap.current.get(task.id)
|
||||||
if (!originalTask) return ''
|
if (!originalTask) return ''
|
||||||
|
|
||||||
const assignee = originalTask.assignee_name || 'Unassigned'
|
const assignee = escapeHtml(originalTask.assignee_name) || 'Unassigned'
|
||||||
const statusName = originalTask.status_name || 'No Status'
|
const statusName = escapeHtml(originalTask.status_name) || 'No Status'
|
||||||
const priority = originalTask.priority.charAt(0).toUpperCase() + originalTask.priority.slice(1)
|
const priority = escapeHtml(originalTask.priority.charAt(0).toUpperCase() + originalTask.priority.slice(1))
|
||||||
|
const taskName = escapeHtml(task.name)
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="gantt-popup">
|
<div class="gantt-popup">
|
||||||
<h3 class="gantt-popup-title">${task.name}</h3>
|
<h3 class="gantt-popup-title">${taskName}</h3>
|
||||||
<div class="gantt-popup-info">
|
<div class="gantt-popup-info">
|
||||||
<div class="gantt-popup-row">
|
<div class="gantt-popup-row">
|
||||||
<span class="gantt-popup-label">Assignee:</span>
|
<span class="gantt-popup-label">Assignee:</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { SkeletonTable } from '../components/Skeleton'
|
import { SkeletonTable } from '../components/Skeleton'
|
||||||
import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit'
|
import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit'
|
||||||
|
import { escapeHtml } from '../utils/escapeHtml'
|
||||||
|
|
||||||
interface AuditLogDetailProps {
|
interface AuditLogDetailProps {
|
||||||
log: AuditLog
|
log: AuditLog
|
||||||
@@ -368,11 +369,11 @@ export default function AuditPage() {
|
|||||||
|
|
||||||
const tableRows = logs.map(log => `
|
const tableRows = logs.map(log => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${formatDate(log.created_at)}</td>
|
<td>${escapeHtml(formatDate(log.created_at))}</td>
|
||||||
<td>${log.event_type}</td>
|
<td>${escapeHtml(log.event_type)}</td>
|
||||||
<td>${log.resource_type} / ${log.resource_id?.substring(0, 8) || '-'}</td>
|
<td>${escapeHtml(log.resource_type)} / ${escapeHtml(log.resource_id?.substring(0, 8)) || '-'}</td>
|
||||||
<td>${log.user_name || 'System'}</td>
|
<td>${escapeHtml(log.user_name) || 'System'}</td>
|
||||||
<td><span style="background-color: ${getSensitivityColor(log.sensitivity_level)}; color: ${log.sensitivity_level === 'medium' ? '#000' : '#fff'}; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${log.sensitivity_level}</span></td>
|
<td><span style="background-color: ${getSensitivityColor(log.sensitivity_level)}; color: ${log.sensitivity_level === 'medium' ? '#000' : '#fff'}; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${escapeHtml(log.sensitivity_level)}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')
|
`).join('')
|
||||||
|
|
||||||
|
|||||||
24
frontend/src/utils/escapeHtml.ts
Normal file
24
frontend/src/utils/escapeHtml.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Escapes HTML special characters to prevent XSS attacks.
|
||||||
|
* Use this when inserting dynamic content into HTML strings.
|
||||||
|
*/
|
||||||
|
export function escapeHtml(unsafe: string | null | undefined): string {
|
||||||
|
if (unsafe == null) return ''
|
||||||
|
return String(unsafe)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes HTML for use in HTML attributes.
|
||||||
|
* More restrictive than escapeHtml - also escapes backticks and equals.
|
||||||
|
*/
|
||||||
|
export function escapeAttr(unsafe: string | null | undefined): string {
|
||||||
|
if (unsafe == null) return ''
|
||||||
|
return escapeHtml(unsafe)
|
||||||
|
.replace(/`/g, '`')
|
||||||
|
.replace(/=/g, '=')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user