fix: update Dropdown.svelte to properly close empty div tag and enhance markdown sanitizer

- Fixed an empty div tag in Dropdown.svelte to ensure proper HTML structure.
- Implemented a custom HTML sanitizer in markdown.ts to avoid postcss dependency issues, allowing only specific tags and attributes to enhance security against XSS attacks.
This commit is contained in:
Sam Chau
2025-11-09 01:11:19 +11:00
parent 92035e8fc5
commit d69064803a
5 changed files with 4795 additions and 16 deletions

View File

@@ -16,12 +16,14 @@
"$utils/": "./src/lib/server/utils/",
"$notifications/": "./src/lib/server/notifications/",
"@std/assert": "jsr:@std/assert@^1.0.0",
"marked": "npm:marked@^12.0.2",
"sanitize-html": "npm:sanitize-html@^2.17.0",
"marked": "npm:marked@^15.0.6",
"simple-icons": "npm:simple-icons@^15.17.0"
},
"tasks": {
"dev": "APP_BASE_PATH=./dist/dev vite dev",
"dev": "APP_BASE_PATH=./dist/dev deno run -A npm:vite dev",
"build": "APP_BASE_PATH=./dist/build deno run -A npm:vite build",
"preview": "APP_BASE_PATH=./dist/data ./dist/build/profilarr",
"compile": "deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --target x86_64-unknown-linux-gnu --output dist/build/profilarr dist/build/mod.ts",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env",

5
deno.lock generated
View File

@@ -17,12 +17,10 @@
"npm:eslint@^9.36.0": "9.39.1",
"npm:globals@^16.4.0": "16.5.0",
"npm:lucide-svelte@0.546": "0.546.0_svelte@5.43.3__acorn@8.15.0",
"npm:marked@^12.0.2": "12.0.2",
"npm:marked@^15.0.6": "15.0.12",
"npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.43.3__acorn@8.15.0",
"npm:prettier-plugin-tailwindcss@~0.6.14": "0.6.14_prettier@3.6.2_prettier-plugin-svelte@3.4.0__prettier@3.6.2__svelte@5.43.3___acorn@8.15.0_svelte@5.43.3__acorn@8.15.0",
"npm:prettier@^3.6.2": "3.6.2",
"npm:sanitize-html@^2.17.0": "2.17.0",
"npm:simple-icons@^15.17.0": "15.17.0",
"npm:svelte-check@^4.3.2": "4.3.3_svelte@5.43.3__acorn@8.15.0_typescript@5.9.3",
"npm:svelte@^5.39.5": "5.43.3_acorn@8.15.0",
@@ -1929,8 +1927,7 @@
"workspace": {
"dependencies": [
"jsr:@std/assert@1",
"npm:marked@^12.0.2",
"npm:sanitize-html@^2.17.0",
"npm:marked@^15.0.6",
"npm:simple-icons@^15.17.0"
],
"packageJson": {

4731
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
<!-- Dropdown content - can be used standalone or within a trigger wrapper -->
<!-- Invisible hover bridge to keep dropdown open when moving mouse down -->
<div class="absolute top-full z-40 h-3 w-full" />
<div class="absolute top-full z-40 h-3 w-full"></div>
<div
class="absolute top-full z-50 mt-3 rounded-lg border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800 {positionClass}"

View File

@@ -3,7 +3,62 @@
*/
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
/**
* Simple HTML sanitizer to avoid postcss dependency issues in compiled binaries
*/
function sanitizeHtml(html: string): string {
// Allowed tags
const allowedTags = new Set([
'p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'blockquote',
'ul', 'ol', 'li', 'a', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'del', 'ins'
]);
// Allowed attributes per tag
const allowedAttrs: Record<string, Set<string>> = {
'a': new Set(['href', 'title']),
'img': new Set(['src', 'alt', 'title'])
};
// Remove script tags and their content
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// Remove event handlers and javascript: URLs
html = html.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
html = html.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, '');
// Filter tags and attributes
return html.replace(/<\/?([a-z][a-z0-9]*)\b([^>]*)>/gi, (match, tag, attrs) => {
const lowerTag = tag.toLowerCase();
// Remove disallowed tags
if (!allowedTags.has(lowerTag)) {
return '';
}
// If closing tag, allow it
if (match.startsWith('</')) {
return `</${lowerTag}>`;
}
// Filter attributes
const allowedForTag = allowedAttrs[lowerTag];
if (!allowedForTag) {
return `<${lowerTag}>`;
}
const filteredAttrs = attrs.replace(/([a-z][a-z0-9-]*)\s*=\s*["']([^"']*)["']/gi,
(attrMatch: string, attrName: string, attrValue: string) => {
if (allowedForTag.has(attrName.toLowerCase())) {
return ` ${attrName}="${attrValue}"`;
}
return '';
});
return `<${lowerTag}${filteredAttrs}>`;
});
}
/**
* Parse markdown to sanitized HTML
@@ -15,13 +70,7 @@ export function parseMarkdown(markdown: string | null | undefined): string {
const html = marked.parse(markdown) as string;
// Sanitize HTML to prevent XSS
return sanitizeHtml(html, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title']
}
});
return sanitizeHtml(html);
}
/**