个人博客 blog.vue2.xyz 的系统级安全升级记录——密码哈希从 SHA256 升级到 bcrypt、统一 JWT 认证、XSS 消毒、路径穿越防护等全链路加固实践。
背景
个人博客 blog.vue2.xyz 经过近一年的迭代,功能日益完善。随着网盘、图床、后台管理等功能上线,认证体系和安全防护逐渐成为需要认真对待的问题。本文记录了 2026 年 6 月的一次安全大升级——从密码哈希到 XSS 消毒、从路径穿越到统一认证,系统性地加固了整个项目。
一、密码哈希:从无盐 SHA256 到 bcrypt
旧方案的问题
原先的密码存储非常简单,就是裸 SHA256:
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
没有盐、没有迭代拉伸、没有常量时间比较。数据库泄露后,密码可被彩虹表或 GPU 暴力破解快速还原。
升级到 bcrypt
引入了 passlib[bcrypt],密码哈希改为 bcrypt,同时保留旧哈希兼容:
from passlib.hash import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
if hashed.startswith("$2b$"):
return bcrypt.verify(plain, hashed)
return hashlib.sha256(plain.encode()).hexdigest() == hashed
自动迁移策略
旧版 SHA256 哈希在用户下次登录时自动升级为 bcrypt,无缝过渡:
if is_legacy_hash(admin.password_hash):
admin.password_hash = hash_password(data.password)
db.commit()
用户无需任何操作,旧密码可以一直用到下次登录,登录瞬间自动完成升级。
二、统一 JWT 认证
旧方案的问题
升级前,不同功能用了不同的认证方式:后台管理走 JWT,图床上传通过 admin_password FormData 字段,网盘上传通过 admin_password URL 查询参数。网盘上传尤其危险——管理员密码通过 URL 参数传递,Nginx 的 access log 和浏览器历史都会留下明文密码。
统一为 JWT
所有上传接口统一改为 Depends(get_current_admin):
@router.post("/upload")
async def upload_pan(
...,
admin: Admin = Depends(get_current_admin),
):
... # 不再需要 admin_password 参数
前端同步更新:每次上传前手动输入密码 → 调用 /api/auth/login 获取 JWT → 带 Authorization: Bearer 头上传。
Cookie 安全标记
const token = useCookie("blog_token", {
maxAge: 60 * 60 * 24 * 7,
sameSite: "lax",
path: "/",
secure: import.meta.dev !== true,
})
三、XSS 消毒
旧方案的问题
文章内容使用 marked 库将 Markdown 转 HTML,然后通过 Vue 的 v-html 直接渲染。marked v14 默认允许原始 HTML,Markdown 中的 script 标签会被直接执行。同样的问题出现在 About 页面、后台预览弹窗等 7 个渲染点。
引入 DOMPurify
选择了 isomorphic-dompurify(SSR 兼容版本),在所有 v-html 渲染前消毒:
import DOMPurify from "isomorphic-dompurify"
const html = computed(() => {
return DOMPurify.sanitize(marked(props.content, { breaks: true }))
})
覆盖 ContentRenderer.vue、文章详情页、关于页、编辑器预览、后台预览弹窗等全部渲染点。
四、路径穿越防护
文件伺服
原始代码直接用用户传入的文件名拼接路径,攻击者可构造 /uploads/../data/blog.db 直接下载整个 SQLite 数据库。
修复
filepath = (UPLOAD_DIR / filename).resolve()
if not str(filepath).startswith(str(UPLOAD_DIR.resolve())):
raise HTTPException(status_code=403, detail="Forbidden")
后台上传文件的 delete/list/cleanup 接口也统一修复。
五、网盘文件类型白名单
网盘上传扩展名黑名单从 11 个扩充到 22 个,新增 .php/.asp/.jsp/.jar/.dll/.wasm 等危险后缀,同时修复了 filename=None 时绕过检测的漏洞。
六、其他加固
SECRET_KEY 运行时检查
生产环境忘记设置 SECRET_KEY 时,后端启动直接报错,防止默认密钥被利用。
CORS 白名单
从 ["*"] 收紧为 [SITE_URL, "http://localhost:3000"],由环境变量控制。
管理员用户名保护
admin-username 端点加上 JWT 认证,防止匿名枚举。
登录支持无用户名
单用户博客场景,登录接口支持不传 username,后端自动匹配唯一管理员。
七、总结
这次安全升级覆盖了从密码存储到前端渲染、从 API 认证到文件访问的完整链路:密码存储从 SHA256 升级到 bcrypt 并支持自动迁移,API 认证统一为 JWT,前端 XSS 通过 DOMPurify 全面消毒,文件访问添加路径穿越防护,配置安全通过运行时检查兜底,CORS 从通配符收紧为白名单。
全部代码已开源于 GitHub。
评论
暂无评论,来写第一条吧
