从一份渗透报告开始的安全加固
从一份渗透报告开始的安全加固
项目:SGCC 智能服务控制台 · 学习记录
起因
攻击方递来一份渗透报告,两个高危:
1 | 1. 目录遍历 |
我看完第一反应是:完了,源码全裸了。
根因:把开发服务器丢到生产
/@fs/ 是 Vite 开发服务器(dev server)专用的源码读取端点,给热更新(HMR)用的。它的设计前提是「跑在开发者本机」,所以默认对源码、node_modules 一律放行。
我们的 frontend/start.sh 末尾是这一行:
1 | npm run dev |
而 npm run dev 实际跑的是:
1 | concurrently "node server/index.js" "vite" |
vite 这个进程是 dev server。它本来该绑 localhost,但 vite.config.ts 里写的是:
1 | server: { |
加上 nginx 把 / 反代到 frontend:3000,相当于把 dev server 的所有暴露面原样搬到了公网。
CVE-2025-30208 就是 Vite 6.2 系列对 ?raw 这种 query 后缀的 server.fs 校验有绕过——即使配置了限制路径,加个 ?raw 就能读任意文件。
一句话总结
dev 工具链是为了开发体验设计的,安全模型默认是「内网/本机」。把它当生产服务器用,等于把一栋只装了纱窗的房子直接放在闹市。
修复方案:分两步
第一步:移除 dev server,改成纯静态 + 反向代理
正确的生产方式:
1 | 浏览器 ─► nginx:80 |
具体动作:
- 在
frontend/package.json里vite build一次,产出dist/ - nginx 的
root指向dist/,try_files兜底走index.html - dev server 完全不在生产容器里跑
nginx.conf 顺带把所有 dev 工具路径主动 404,做兜底:
1 | location ~ ^/(@fs|@vite|@id|@react-refresh|node_modules|__vite_ping|src/) { |
这样即使将来误操作把 dev server 起来了,公网也访问不到这些路径。
第二步:升级 Vite + 收紧本地 dev
1 | "vite": "^6.4.2" // 修掉 CVE-2025-30208 |
本地开发的 vite.config.ts:
1 | server: { |
「即使本地开发也按生产标准防御」——多一道门没坏处。
顺带发现的其他坑
挨个审过一遍代码后,又拎出来不少问题。下面是当时记的清单:
1. CORS 反射 + credentials
1 | cors({ origin: true, credentials: true }) // ❌ |
origin: true 把请求来源原样回写到 Access-Control-Allow-Origin。配合 credentials: true,等于允许任意网站带着用户 cookie 调你的 API。
虽然 cookie 本身有 SameSite=Strict 兜底,但配置本身是反模式。
修法:精确白名单。
1 | const allowedOrigins = (process.env.ALLOWED_ORIGINS || '') |
默认根本不启用 CORS(同源部署不需要),只有显式配置 ALLOWED_ORIGINS 才放行。
2. AES 密钥可退化为 scrypt + 固定盐
老代码:
1 | const key = Buffer.from(ENCRYPTION_KEY, 'hex').length === 32 |
如果 ENCRYPTION_KEY 不是 64-hex,就走 scrypt 派生,salt 是字面量 'salt'——等于无盐 KDF,多实例可预测。
修法:直接拒绝。
1 | if (rawEncryptionKey && /^[0-9a-f]{64}$/i.test(rawEncryptionKey)) { |
部署文档同步给生成命令:openssl rand -hex 32。
3. 验证码与登录无任何速率限制
POST /api/auth/send-code 没限流——攻击者刷你的 SMTP 配额,把发件人邮箱送进黑名单。
POST /api/auth/login 也没限流——6 位验证码 100 万种组合,3 分钟有效,理论上能在窗口内穷举。
修法:express-rate-limit + 单码尝试计数。
1 | const sendCodeLimiter = rateLimit({ |
4. 客户端可任意修改 record_path / transcript
PUT /api/calls/:id 的字段白名单原本包含 record_path、transcript、ali_transcript、conversation_id。一个登录用户能把某条记录的 record_path 改成任意字符串,配合下游的录音流式接口潜在地枚举服务器文件。
修法:把这些字段从客户端可写白名单里删掉——它们只该被后端流水线写入。
1 | const allowed = new Set([ |
做权限设计时永远从「客户端能写哪些字段」反推,而不是「数据库有哪些字段」——这两个集合不是同一个。
5. 错误回显泄露内部信息
老代码到处是:
1 | res.status(500).json({ error: '服务器内部错误', details: err.message }); |
err.message 会带出 SQL、文件路径、第三方库内部信息。生产环境屏蔽:
1 | const sendServerError = (res, err, requestId) => { |
详细信息只进日志,响应体里只留 requestId 用于排查。
6. multer 没限大小
1 | const upload = multer({ storage: multer.memoryStorage() }); // ❌ |
文件存内存 + 没限大小。攻击者上传一个 1GB 文件就能 OOM。
1 | const upload = multer({ |
7. helmet 缺失,没有任何安全响应头
加上:
1 | app.use(helmet({ |
nginx 这边补上:
1 | add_header X-Content-Type-Options "nosniff" always; |
8. 数据库默认弱口令 + 5432 对宿主机暴露
老的 docker-compose.yaml:
1 | db: |
修法:
- 凭据全部走
${VAR:?...}强制从.env注入,缺失即 fail-fast - 删掉
ports,DB 只在 docker 内部网络通信
1 | db: |
9. 邮箱白名单硬编码到代码
老代码:
1 | const ALLOWED_EMAILS = [ |
这种东西不是配置而是数据,进了 git 历史就等于对外公开「谁能登录这个系统」。
修法:纯环境变量驱动,仓库里清空。
1 | const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '') |
收获与方法论
把上面这些事情摞在一起,能看出几个反复出现的模式:
1. 「最小暴露面」是一道铁律
- Vite dev server 不该上生产
- DB 端口不该绑宿主机
- API 端口不该绑宿主机
- 错误信息不该原样回客户端
- 字段白名单不该等于「所有 DB 列」
每加一个对外可见的东西,都问一句:这个真的有人需要从外面访问吗?
2. 「fail closed」优于「fail open」
- 邮箱白名单为空 → 拒绝所有登录(不是放行所有)
ENCRYPTION_KEY不合规 → 直接拒启动(不是悄悄派生一个弱 key)- DB 凭据缺失 → 直接拒启动(不是用
admin/password兜底) - 文件路径越界 → 404(不是尝试解析一下看能不能读)
3. 安全是「分层」的
同一道防御,单层失效不该意味着系统失守:
- Vite
fs.strict+ nginx 路径 404 + 生产不跑 dev = 三层都得突破才能目录遍历 - cookie
httpOnly+Secure+SameSite=Strict= 三个独立向量都得绕过才能盗 session - 路径校验
path.basename+ 允许目录白名单二次校验 = 拼路径攻击得绕过两道
4. 配置 ≠ 代码
- 邮箱白名单这种「会变的、不该被开发者知道的」放配置
- 加密算法、流程这种「不该频繁变的」放代码
- 把数据塞进代码 = 把数据公开
5. 学会读 CVE
CVE-2025-30208 不是天降的——它在 Vite 6.4.2 / 7.3.2 / 8.0.5 / 6.2.3 等版本一发布修复就有对应 CHANGELOG。npm audit 跑一下、docker scan 跑一下,至少把这一类批量发现的漏洞拦在自己的部署之外。
参考索引
| 修复点 | 主要文件 |
|---|---|
| 移除 dev server / 切静态托管 | docker-compose.yaml, nginx/nginx.conf |
| Vite 升级 + fs.strict | frontend/package.json, frontend/vite.config.ts |
| CORS / helmet / 限流 / 错误回显 | frontend/server/index.js |
| 加密密钥强约束 | 同上 |
| 字段白名单收紧 | frontend/server/models.js |
| 完整部署清单 | docs/DEPLOYMENT.md |








