Files
ai-tutor-template-244/prototype/view-doc.html
Claude Code 6e9be01a09 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>
2026-03-29 04:27:18 +00:00

298 lines
9.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档查看器 - AI 智能抽背助手</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.0.0/dist/markdown-it.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Markdown 内容样式 */
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #24292f;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 {
font-size: 2em;
border-bottom: 1px solid #d8dee4;
padding-bottom: 0.3em;
}
.markdown-body h2 {
font-size: 1.5em;
border-bottom: 1px solid #d8dee4;
padding-bottom: 0.3em;
}
.markdown-body h3 {
font-size: 1.25em;
}
.markdown-body h4 {
font-size: 1em;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
}
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #1f2937;
border-radius: 6px;
margin-bottom: 16px;
}
.markdown-body pre code {
background-color: transparent;
padding: 0;
color: #e5e7eb;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body li {
margin-top: 0.25em;
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
margin-top: 0;
margin-bottom: 16px;
width: 100%;
}
.markdown-body table th {
font-weight: 600;
background-color: #f6f8fa;
border: 1px solid #d8dee4;
padding: 6px 13px;
}
.markdown-body table td {
border: 1px solid #d8dee4;
padding: 6px 13px;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body blockquote {
padding: 0 1em;
color: #57606a;
border-left: 0.25em solid #d8dee4;
margin: 0 0 16px 0;
}
.markdown-body a {
color: #0969da;
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-body hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d8dee4;
border: 0;
}
/* 加载动画 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 错误提示 */
.error {
background-color: #fee;
border: 1px solid #fcc;
padding: 16px;
border-radius: 6px;
color: #c33;
text-align: center;
}
</style>
</head>
<body class="bg-gray-50">
<!-- 顶部导航栏 -->
<nav class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center gap-4">
<a href="docs-index.html" class="text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left"></i> 返回文档中心
</a>
<h1 id="doc-title" class="text-xl font-semibold flex-1">文档加载中...</h1>
<div class="flex items-center gap-2">
<button onclick="copyLink()" class="px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
<i class="fas fa-link mr-1"></i>复制链接
</button>
<button onclick="printDoc()" class="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">
<i class="fas fa-print mr-1"></i>打印
</button>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="max-w-7xl mx-auto px-4 py-8">
<!-- 加载动画 -->
<div id="loading" class="loading">
<div class="loading-spinner"></div>
</div>
<!-- 错误提示 -->
<div id="error" class="error hidden"></div>
<!-- Markdown 内容 -->
<article id="content" class="markdown-body bg-white rounded-xl shadow-sm p-8 hidden"></article>
</main>
<!-- 页脚 -->
<footer class="text-center py-6 text-sm text-gray-500">
© 2026 AI 智能抽背助手 | 基于 markdown-it 渲染
</footer>
<script>
// 初始化 markdown-it - 安全配置(禁用 HTML 以防止 XSS
const md = window.markdownit({
html: false, // 禁用 HTML 标签以防止 XSS
linkify: true,
typographer: true,
breaks: true
});
// 获取 URL 参数
function getQueryParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
// 复制链接
function copyLink() {
const url = window.location.href;
navigator.clipboard.writeText(url)
.then(() => {
alert('链接已复制到剪贴板');
})
.catch(err => {
console.error('复制失败:', err);
});
}
// 打印文档
function printDoc() {
window.print();
}
// 显示错误信息
function showError(message) {
document.getElementById('loading').classList.add('hidden');
document.getElementById('error').classList.remove('hidden');
document.getElementById('error').textContent = message;
}
// 加载并渲染文档 - 优化:并行尝试所有路径
async function loadAndRenderDoc() {
const docPath = getQueryParam('doc');
if (!docPath) {
showError('未指定文档路径,请使用 ?doc=文件名.md 参数访问');
return;
}
// 验证 docPath 安全性 - 防止路径遍历
if (docPath.includes('..') || docPath.includes('/') || docPath.includes('\\')) {
showError('无效的文档路径');
return;
}
// 从文件名提取标题
const title = docPath
.replace(/\.md$/, '')
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
document.getElementById('doc-title').textContent = title;
// 尝试多个路径查找文档 - 并行请求提高效率
const possiblePaths = [
`../planning-artifacts/${docPath}`,
`../docs/${docPath}`,
`../${docPath}`,
`planning-artifacts/${docPath}`,
`docs/${docPath}`,
docPath
];
// 并行尝试所有路径,使用 Promise.race 获取第一个成功的结果
const fetchPromises = possiblePaths.map(async (path) => {
try {
const response = await fetch(path);
if (response.ok) {
return { path, markdown: await response.text() };
}
return null;
} catch {
return null;
}
});
const results = await Promise.all(fetchPromises);
const successfulResult = results.find(r => r !== null);
if (!successfulResult) {
showError('无法加载文档,请检查文件名是否正确');
return;
}
// 渲染 Markdownhtml: 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;
}
// 页面加载完成后执行
window.addEventListener('DOMContentLoaded', loadAndRenderDoc);
</script>
</body>
</html>