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:
beabigegg
2026-01-13 21:26:06 +08:00
parent a78d878865
commit 3da0bf5c3a
3 changed files with 36 additions and 9 deletions

View File

@@ -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>

View File

@@ -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('')

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
/**
* 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, '&#96;')
.replace(/=/g, '&#61;')
}