security: 增强 server.js 和 view-doc.html 的安全性与性能
server.js 改进:
- 使用标准 URL API 代替简单的 split('?')[0]
- 添加路径遍历攻击防护(path.normalize + 路径检查)
- 改进错误处理,不暴露内部错误详情
- 添加 try-catch 保护 URL 解析
view-doc.html 改进:
- markdown-it 配置 html: false 防止 XSS 攻击
- 添加文档路径安全验证(检查 .. / \ 字符)
- 使用并行请求代替串行(Promise.all)
- 使用 textContent 设置标题防止 XSS
- 优化错误提示信息
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
34
prototype/server.js
vendored
34
prototype/server.js
vendored
@@ -19,6 +19,7 @@ const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { URL } = require('url');
|
||||
|
||||
const PORT = 8080;
|
||||
const PROTOTYPE_DIR = __dirname;
|
||||
@@ -44,16 +45,35 @@ const MIME_TYPES = {
|
||||
|
||||
// 创建服务器
|
||||
const server = http.createServer((req, res) => {
|
||||
// 解析 URL
|
||||
let urlPath = req.url.split('?')[0] || '/';
|
||||
// 使用 URL 模块安全解析 URL
|
||||
let urlPath;
|
||||
try {
|
||||
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||||
urlPath = parsedUrl.pathname || '/';
|
||||
} catch {
|
||||
urlPath = '/';
|
||||
}
|
||||
|
||||
// 处理根路径
|
||||
if (urlPath === '/') {
|
||||
urlPath = '/index.html';
|
||||
}
|
||||
|
||||
// 构建文件路径
|
||||
const filePath = urlPath.startsWith("/planning-artifacts/") ? path.join(PARENT_DIR, urlPath) : path.join(PROTOTYPE_DIR, urlPath);
|
||||
// 安全验证:防止路径遍历攻击
|
||||
// 规范化路径并检查是否在允许的目录内
|
||||
const isPlanningArtifacts = urlPath.startsWith("/planning-artifacts/");
|
||||
const baseDir = isPlanningArtifacts ? PARENT_DIR : PROTOTYPE_DIR;
|
||||
const relativePath = isPlanningArtifacts ? urlPath : urlPath;
|
||||
|
||||
const normalizedPath = path.normalize(path.join(baseDir, relativePath));
|
||||
if (!normalizedPath.startsWith(baseDir)) {
|
||||
// 路径遍历攻击检测
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('<!DOCTYPE html><html><head><meta charset="UTF-8"><title>400 - 错误请求</title></head><body><h1>400 - 错误请求</h1><p>无效的路径</p></body></html>');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = normalizedPath;
|
||||
const extname = path.extname(filePath).toLowerCase();
|
||||
|
||||
// 设置响应头
|
||||
@@ -104,16 +124,16 @@ const server = http.createServer((req, res) => {
|
||||
<body>
|
||||
<div class="error-box">
|
||||
<h1>404</h1>
|
||||
<p>文件未找到: ${urlPath}</p>
|
||||
<p>文件未找到</p>
|
||||
<a href="/">返回首页</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
// 服务器错误
|
||||
// 服务器错误 - 不暴露内部错误详情
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('服务器错误: ' + err.code);
|
||||
res.end('<!DOCTYPE html><html><head><meta charset="UTF-8"><title>500 - 服务器错误</title></head><body><h1>500 - 服务器错误</h1><p>服务器内部错误,请稍后重试</p></body></html>');
|
||||
}
|
||||
} else {
|
||||
// 成功返回文件
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// 初始化 markdown-it
|
||||
// 初始化 markdown-it - 安全配置(禁用 HTML 以防止 XSS)
|
||||
const md = window.markdownit({
|
||||
html: true,
|
||||
html: false, // 禁用 HTML 标签以防止 XSS
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
@@ -229,7 +229,7 @@
|
||||
document.getElementById('error').textContent = message;
|
||||
}
|
||||
|
||||
// 加载并渲染文档
|
||||
// 加载并渲染文档 - 优化:并行尝试所有路径
|
||||
async function loadAndRenderDoc() {
|
||||
const docPath = getQueryParam('doc');
|
||||
if (!docPath) {
|
||||
@@ -237,6 +237,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 docPath 安全性 - 防止路径遍历
|
||||
if (docPath.includes('..') || docPath.includes('/') || docPath.includes('\\')) {
|
||||
showError('无效的文档路径');
|
||||
return;
|
||||
}
|
||||
|
||||
// 从文件名提取标题
|
||||
const title = docPath
|
||||
.replace(/\.md$/, '')
|
||||
@@ -244,7 +250,7 @@
|
||||
.replace(/\b\w/g, l => l.toUpperCase());
|
||||
document.getElementById('doc-title').textContent = title;
|
||||
|
||||
// 尝试多个路径查找文档
|
||||
// 尝试多个路径查找文档 - 并行请求提高效率
|
||||
const possiblePaths = [
|
||||
`../planning-artifacts/${docPath}`,
|
||||
`../docs/${docPath}`,
|
||||
@@ -254,32 +260,31 @@
|
||||
docPath
|
||||
];
|
||||
|
||||
let markdown = null;
|
||||
let loadedPath = null;
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
// 并行尝试所有路径,使用 Promise.race 获取第一个成功的结果
|
||||
const fetchPromises = possiblePaths.map(async (path) => {
|
||||
try {
|
||||
const response = await fetch(path);
|
||||
if (response.ok) {
|
||||
markdown = await response.text();
|
||||
loadedPath = path;
|
||||
break;
|
||||
return { path, markdown: await response.text() };
|
||||
}
|
||||
} catch (err) {
|
||||
// 继续尝试下一个路径
|
||||
continue;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!markdown) {
|
||||
showError(`无法加载文档: ${docPath}<br>已尝试以下路径:<br>${possiblePaths.join('<br>')}`);
|
||||
const results = await Promise.all(fetchPromises);
|
||||
const successfulResult = results.find(r => r !== null);
|
||||
|
||||
if (!successfulResult) {
|
||||
showError('无法加载文档,请检查文件名是否正确');
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染 Markdown
|
||||
const html = md.render(markdown);
|
||||
// 渲染 Markdown(html: false 配置已防止 XSS)
|
||||
const html = md.render(successfulResult.markdown);
|
||||
|
||||
// 显示内容
|
||||
// 显示内容 - 使用 textContent 设置标题防止 XSS
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
document.getElementById('content').classList.remove('hidden');
|
||||
document.getElementById('content').innerHTML = html;
|
||||
|
||||
Reference in New Issue
Block a user