diff --git a/docs/images/pipeline.png b/docs/images/pipeline.png new file mode 100644 index 000000000..ae9c31692 Binary files /dev/null and b/docs/images/pipeline.png differ diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 9593dbc5e..ad9a46953 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -3,6 +3,13 @@ server { server_name localhost; client_max_body_size 50M; + # 安全头配置 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; frame-ancestors 'self';" always; + # 错误日志配置 error_log /var/log/nginx/error.log warn; access_log /var/log/nginx/access.log; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bad250f96..d16df4bee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", + "@types/dompurify": "^3.0.5", "axios": "^1.8.4", + "dompurify": "^3.2.6", "marked": "^5.1.2", "pagefind": "^1.1.1", "pinia": "^3.0.1", @@ -1274,6 +1276,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://mirrors.tencent.com/npm/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://mirrors.tencent.com/npm/@types/eslint/-/eslint-9.6.1.tgz", @@ -1346,6 +1357,12 @@ "resolved": "https://mirrors.tencent.com/npm/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://mirrors.tencent.com/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://mirrors.tencent.com/npm/@types/validator/-/validator-13.15.2.tgz", @@ -2121,6 +2138,15 @@ "node": ">=0.4.0" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://mirrors.tencent.com/npm/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://mirrors.tencent.com/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 64f264b39..de5aa2622 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,9 @@ }, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", + "@types/dompurify": "^3.0.5", "axios": "^1.8.4", + "dompurify": "^3.2.6", "marked": "^5.1.2", "pagefind": "^1.1.1", "pinia": "^3.0.1", diff --git a/frontend/src/components/doc-content.vue b/frontend/src/components/doc-content.vue index 5c73e3553..283091a04 100644 --- a/frontend/src/components/doc-content.vue +++ b/frontend/src/components/doc-content.vue @@ -4,6 +4,8 @@ import { onMounted, ref, nextTick, onUnmounted, onUpdated, watch } from "vue"; import { downKnowledgeDetails } from "@/api/knowledge-base/index"; import { MessagePlugin } from "tdesign-vue-next"; import picturePreview from '@/components/picture-preview.vue'; +import { sanitizeHTML, safeMarkdownToHTML, createSafeImage, isValidImageURL } from '@/utils/security'; + marked.use({ mangle: false, headerIds: false, @@ -37,10 +39,16 @@ const checkImage = (url) => { }); }; renderer.image = function (href, title, text) { - // 自定义HTML结构,图片展示带标题 + // 安全地处理图片链接 + if (!isValidImageURL(href)) { + return `
无效的图片链接
`; + } + + // 使用安全的图片创建函数 + const safeImage = createSafeImage(href, text || '', title || ''); return `