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:
3
public/js/purify.min.js
vendored
Normal file
3
public/js/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
|||||||
popToast(custom) {
|
popToast(custom) {
|
||||||
let html = '';
|
let html = '';
|
||||||
if (typeof custom != 'undefined') {
|
if (typeof custom != 'undefined') {
|
||||||
html = custom;
|
html = window.sanitizeHTML(custom);
|
||||||
}
|
}
|
||||||
toast(this.title, { description: this.description, type: this.type, position: this.position, html: html })
|
toast(this.title, { description: this.description, type: this.type, position: this.position, html: html })
|
||||||
}
|
}
|
||||||
@@ -276,13 +276,16 @@
|
|||||||
if(event.detail.position){
|
if(event.detail.position){
|
||||||
position = 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({
|
toasts.unshift({
|
||||||
id: 'toast-' + Math.random().toString(16).slice(2),
|
id: 'toast-' + Math.random().toString(16).slice(2),
|
||||||
show: false,
|
show: false,
|
||||||
message: event.detail.message,
|
message: event.detail.message,
|
||||||
description: event.detail.description,
|
description: event.detail.description,
|
||||||
type: event.detail.type,
|
type: event.detail.type,
|
||||||
html: event.detail.html
|
html: sanitizedHtml
|
||||||
});
|
});
|
||||||
"
|
"
|
||||||
@mouseenter="toastsHovered=true;" @mouseleave="toastsHovered=false" x-init="if (layout == 'expanded') {
|
@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"
|
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>
|
fill="currentColor"></path>
|
||||||
</svg>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
|
<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"
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="toast.html">
|
<template x-if="toast.html">
|
||||||
<div x-html="toast.html"></div>
|
<div x-html="window.sanitizeHTML(toast.html)"></div>
|
||||||
</template>
|
</template>
|
||||||
<span class="absolute mt-1 text-xs right-[4.4rem] text-success font-bold"
|
<span class="absolute mt-1 text-xs right-[4.4rem] text-success font-bold"
|
||||||
x-show="copyNotification"
|
x-show="copyNotification"
|
||||||
|
@@ -35,9 +35,9 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
|
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
|
||||||
@env('local')
|
@env('local')
|
||||||
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/x-icon" />
|
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/x-icon" />
|
||||||
@else
|
@else
|
||||||
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/x-icon" />
|
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/x-icon" />
|
||||||
@endenv
|
@endenv
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
<script type="text/javascript" src="{{ URL::asset('js/echo.js') }}"></script>
|
<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/pusher.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ URL::asset('js/apexcharts.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
|
@endauth
|
||||||
</head>
|
</head>
|
||||||
@section('body')
|
@section('body')
|
||||||
@@ -61,6 +62,32 @@
|
|||||||
<body>
|
<body>
|
||||||
<x-toast />
|
<x-toast />
|
||||||
<script data-navigate-once>
|
<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)) {
|
if (!('theme' in localStorage)) {
|
||||||
localStorage.theme = 'dark';
|
localStorage.theme = 'dark';
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
<form wire:submit='submit' class="flex flex-col pb-10">
|
<form wire:submit='submit' class="flex flex-col pb-10">
|
||||||
<div class="flex gap-2">
|
<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">
|
<div class="flex items-end gap-2">
|
||||||
<x-forms.button type="submit">Save</x-forms.button>
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />
|
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
<form wire:submit='submit' class="flex flex-col">
|
<form wire:submit='submit' class="flex flex-col">
|
||||||
<div class="flex items-end gap-2">
|
<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>
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
|
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<x-resource-view>
|
<x-resource-view>
|
||||||
<x-slot:title><span x-text="application.name"></span></x-slot>
|
<x-slot:title><span x-text="application.name"></span></x-slot>
|
||||||
<x-slot:description>
|
<x-slot:description>
|
||||||
<span x-html="application.description"></span>
|
<span x-html="window.sanitizeHTML(application.description)"></span>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
<x-slot:logo>
|
<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"
|
<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:description><span x-text="database.description"></span></x-slot>
|
||||||
<x-slot:logo>
|
<x-slot:logo>
|
||||||
<span x-show="database.logo">
|
<span x-show="database.logo">
|
||||||
<span x-html="database.logo"></span>
|
<span x-html="window.sanitizeHTML(database.logo)"></span>
|
||||||
</span>
|
</span>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
</x-resource-view>
|
</x-resource-view>
|
||||||
|
@@ -303,7 +303,7 @@
|
|||||||
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
|
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dark:text-neutral-300 leading-relaxed max-w-none"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -69,7 +69,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col pt-4" x-show="showProgress">
|
<div class="flex flex-col pt-4" x-show="showProgress">
|
||||||
<h2>Progress <x-loading /></h2>
|
<h2>Progress <x-loading /></h2>
|
||||||
<div x-html="currentStatus"></div>
|
<div x-html="window.sanitizeHTML(currentStatus)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4" x-show="!showProgress">
|
<div class="flex gap-4" x-show="!showProgress">
|
||||||
|
Reference in New Issue
Block a user