feat(sanitization): integrate DOMPurify for HTML sanitization across components

- Added DOMPurify library to sanitize HTML content in toast notifications and other components to prevent XSS vulnerabilities.
- Updated relevant components to use the new `sanitizeHTML` function for safe rendering of HTML content.
- Ensured that only allowed tags and attributes are permitted in sanitized output.
This commit is contained in:
Andras Bacsai
2025-08-19 10:34:54 +02:00
parent f02c36985f
commit 6727fd958f
8 changed files with 77 additions and 44 deletions

3
public/js/purify.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
popToast(custom) {
let html = '';
if (typeof custom != 'undefined') {
html = custom;
html = window.sanitizeHTML(custom);
}
toast(this.title, { description: this.description, type: this.type, position: this.position, html: html })
}
@@ -276,13 +276,16 @@
if(event.detail.position){
position = event.detail.position;
}
// Sanitize HTML content to prevent XSS
let sanitizedHtml = event.detail.html ? window.sanitizeHTML(event.detail.html) : '';
toasts.unshift({
id: 'toast-' + Math.random().toString(16).slice(2),
show: false,
message: event.detail.message,
description: event.detail.description,
type: event.detail.type,
html: event.detail.html
html: sanitizedHtml
});
"
@mouseenter="toastsHovered=true;" @mouseleave="toastsHovered=false" x-init="if (layout == 'expanded') {
@@ -421,16 +424,16 @@
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z"
fill="currentColor"></path>
</svg>
<p class="text-black leading-2 dark:text-neutral-200" x-html="toast.message">
<p class="text-black leading-2 dark:text-neutral-200" x-text="toast.message">
</p>
</div>
<div x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
class="mt-1.5 text-xs px-2 opacity-90 whitespace-pre-wrap w-full break-words"
x-html="toast.description"></div>
x-html="window.sanitizeHTML(toast.description)"></div>
</div>
</template>
<template x-if="toast.html">
<div x-html="toast.html"></div>
<div x-html="window.sanitizeHTML(toast.html)"></div>
</template>
<span class="absolute mt-1 text-xs right-[4.4rem] text-success font-bold"
x-show="copyNotification"

View File

@@ -54,6 +54,7 @@
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
<script type="text/javascript" src="{{ URL::asset('js/pusher.js') }}"></script>
<script type="text/javascript" src="{{ URL::asset('js/apexcharts.js') }}"></script>
<script type="text/javascript" src="{{ URL::asset('js/purify.min.js') }}"></script>
@endauth
</head>
@section('body')
@@ -61,6 +62,32 @@
<body>
<x-toast />
<script data-navigate-once>
// Global HTML sanitization function using DOMPurify
window.sanitizeHTML = function(html) {
if (!html) return '';
// Use DOMPurify with strict configuration for toast notifications
const purified = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'p', 'pre', 's', 'span',
'strong', 'u'
],
ALLOWED_ATTR: ['class', 'href', 'target', 'title'],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'object', 'embed', 'applet', 'iframe', 'form', 'input', 'button',
'select', 'textarea', 'details', 'summary', 'dialog', 'style'
],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange',
'onsubmit', 'ontoggle', 'style'
],
KEEP_CONTENT: true,
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
SANITIZE_DOM: true
});
console.log(purified);
return purified;
};
if (!('theme' in localStorage)) {
localStorage.theme = 'dark';
document.documentElement.classList.add('dark')

View File

@@ -4,7 +4,7 @@
</x-slot>
<form wire:submit='submit' class="flex flex-col pb-10">
<div class="flex gap-2">
<h1>Project: {{ data_get($project, 'name') }}</h1>
<h1>Project: {{ data_get_str($project, 'name')->limit(15) }}</h1>
<div class="flex items-end gap-2">
<x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />

View File

@@ -4,7 +4,7 @@
</x-slot>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-end gap-2">
<h1>Environment: {{ data_get($environment, 'name') }}</h1>
<h1>Environment: {{ data_get_str($environment, 'name')->limit(15) }}</h1>
<x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div>

View File

@@ -29,7 +29,7 @@
<x-resource-view>
<x-slot:title><span x-text="application.name"></span></x-slot>
<x-slot:description>
<span x-html="application.description"></span>
<span x-html="window.sanitizeHTML(application.description)"></span>
</x-slot>
<x-slot:logo>
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:bg-white/10 bg-black/10"
@@ -66,7 +66,7 @@
<x-slot:description><span x-text="database.description"></span></x-slot>
<x-slot:logo>
<span x-show="database.logo">
<span x-html="database.logo"></span>
<span x-html="window.sanitizeHTML(database.logo)"></span>
</span>
</x-slot>
</x-resource-view>

View File

@@ -303,7 +303,7 @@
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
</div>
<div class="dark:text-neutral-300 leading-relaxed max-w-none"
x-html="entry.content_html">
x-html="window.sanitizeHTML(entry.content_html)">
</div>
</div>

View File

@@ -69,7 +69,7 @@
</p>
<div class="flex flex-col pt-4" x-show="showProgress">
<h2>Progress <x-loading /></h2>
<div x-html="currentStatus"></div>
<div x-html="window.sanitizeHTML(currentStatus)"></div>
</div>
</div>
<div class="flex gap-4" x-show="!showProgress">