feat: SAC 0.7.2 security hardening (rate limit, JWT DB check, DOMPurify)

Path traversal fix in SPA fallback, admin-only host delete, login rate limit
with 3 attempts and Telegram alert, JWT validated against active users in DB,
and DOMPurify for agent report HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 11:04:14 +10:00
parent 06a8ed8614
commit d3a337992c
19 changed files with 554 additions and 98 deletions
+31 -2
View File
@@ -1,17 +1,19 @@
{
"name": "sac-ui",
"version": "0.1.0",
"version": "0.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sac-ui",
"version": "0.1.0",
"version": "0.7.2",
"dependencies": {
"dompurify": "^3.2.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/dompurify": "^3.2.0",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.7.2",
"vite": "^6.0.3",
@@ -901,6 +903,17 @@
"win32"
]
},
"node_modules/@types/dompurify": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz",
"integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==",
"deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
"dev": true,
"license": "MIT",
"dependencies": {
"dompurify": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -908,6 +921,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -1130,6 +1150,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
"integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "sac-ui",
"private": true,
"version": "0.7.1",
"version": "0.7.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"dompurify": "^3.2.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
+24 -14
View File
@@ -1,20 +1,30 @@
/** Безопасный вывод HTML из агента (только базовая разметка). */
/** Безопасный вывод HTML из агента (DOMPurify, белый список тегов). */
import DOMPurify from "dompurify";
const ALLOWED_REPORT_TAGS = [
"b",
"strong",
"i",
"em",
"u",
"code",
"pre",
"br",
"p",
"div",
"span",
"ul",
"ol",
"li",
"hr",
];
export function sanitizeAgentHtml(html: string): string {
const allowed = /^(b|strong|i|em|u|code|pre|br|p|div|span|ul|ol|li|hr)$/i;
return html.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, (tag, name: string) => {
if (allowed.test(name)) {
if (tag.toLowerCase().startsWith("</")) {
return `</${name.toLowerCase()}>`;
}
if (name.toLowerCase() === "br") {
return "<br>";
}
return `<${name.toLowerCase()}>`;
}
return "";
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ALLOWED_REPORT_TAGS,
ALLOWED_ATTR: [],
});
}
export interface DailyReportStats {
platform?: "ssh" | "windows";
successful_logins?: number;
+1 -1
View File
@@ -1,3 +1,3 @@
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.7.1";
export const APP_VERSION = "0.7.2";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;