开头:作品集网站也是生产系统
做前端十年,我有个习惯:自己写的东西,先当黑客打一遍。我的个人博客 blog.alleria.cn 是用 Node.js + Express + Markdown 搭的,也是求职作品集的总入口,简历上就挂着这个域名。既然要拿给面试官看,它首先得像个正经工程师搭的系统,而不是一个能跑起来的玩具。
今天我给它做了一轮安全体检,结论有点扎心:几个看似不起眼的设计,把源码和配置直接敞在了公网。
问题一:slug 路径穿越
文章 URL 是 /post/:slug,slug 由文章文件名生成。我最初的路由实现几乎没做校验,slug 直接拼进文件读取路径。这意味着 /post/../server.js 这类请求能直接读到服务端源码,包括里面可能写死的密钥和配置。这种漏洞在生产环境是高危的,而我自己的作品集站也中招了。
修法是加一个 sanitizeSlug(),正则限定为 [\w\u4e00-\u9fa5\-_],把 /post/:slug、/api/posts/:slug 的 GET/PUT/DELETE,以及新建文章生成 slug 这 5 个入口全部统一收敛到这条规则上。一处校验,五处兜底,比在每处手写判断稳得多。
问题二:列表排序错乱
原来 getAllPosts() 按文件名做字母序,中文标题的 slug 一多就乱序,新文章经常沉底。改成读取 frontmatter 的 date 字段倒序:
posts.sort((a, b) => b.date.localeCompare(a.date));
一行改动,排序稳定,中文 slug 不再乱跳。
问题三、四:静态目录过度暴露
更隐蔽的是 express.static(__dirname)。这一行把整个项目根目录挂成了静态资源,server.js、package.json、posts-md 文章源码全都能被 HTTP 直接拉到(实测返回 200)。我把它改成白名单:只挂载 css、js、projects、admin 四个目录,根目录仅显式放行 index.html、about.html、resume.pdf。删掉历史遗留的 blog.html 和 posts/ 下旧文章页,再补两条 301 把旧链接导流到新路由:
app.use('/css', express.static(path.join(__dirname, 'css')));
app.use('/js', express.static(path.join(__dirname, 'js')));
// 根目录白名单
app.get('/blog.html', (req, res) => res.redirect(301, '/blog'));
改完实测 /server.js 返回 404,源码不再裸奔。
部署链路与端口坑
本地 commit 推到 Gitee,再上腾讯云 Ubuntu 服务器 git pull、装依赖、pm2 重启。这里踩了个经典坑:Nginx 反代到 127.0.0.1:3002,但 server.js 默认监听 3000,靠 dotenv 读 .env 里的 PORT=3002 才生效。如果直接 node server.js 启动,端口对不上,Nginx 直接 502。所以重启必须用 pm2 的 blog 进程(cwd 指向项目根),让 .env 被读到。本地 curl 验证时本机代理会拦截 localhost 返回 502,需用 curl --noproxy '*' 绕过。
方法论沉淀
一轮下来,16 条路由全部符合预期(200/301/404)。这次加固给我的核心沉淀就一条:凡是用户可控输入能拼进文件路径、能决定静态资源暴露范围的地方,都默认按最小可见面处理。作品集网站也是生产系统,面试官能 curl 到的,不该包括你的 server.js。