Security and Hardening(安全加固)
Overview
把每个外部输入当敌对,每个 secret 当圣物,每次鉴权当必查。安全不是阶段,是压在每行碰用户数据、鉴权、外部系统代码上的约束。先画威胁模型再上手段:没说清信任边界的功能不配谈安全。
When to Use
- 碰用户输入、文件路径、shell 命令拼接时
- 存 token/secret、做鉴权、调外部 API 时
- 加文件上传、webhook、回调时
- 审计依赖漏洞(本仓包管理器原生审计)时
- 合并前做安全 review 时
When NOT to use:
- 纯文档、注释、格式改动
- 已有威胁模型且本次不碰边界,只是内部重构
威胁模型先行(5 分钟,动码前做)
- 画信任边界:不可信数据从哪进?CLI 参数、环境变量、HTTP 请求、文件、第三方 API、LLM 输出。每条边界即攻击面。
- 点资产:什么值得偷/搞?token、凭据、PII、管理员动作、发版产物。
- STRIDE 速扫(镜头不是仪式):仿冒/篡改/否认/泄露/DoS/提权,每条问一句"能吗",答不上即缺手段。
- 写 abuse case:每个功能问"我怎么滥用它",滥用即第一个测试。
说不清边界即 OWASP A04(不安全设计),停手先画边界。
三级边界(Always / Ask / Never)
Always(跨信任边界的安全默认)
"无例外"指安全默认不许妥协,不是物理世界无例外:本机 IPC / Unix socket / 可信隔离网络等边界内传输本就不过这条"外部通信"线(见首条限定)。真正的例外必须先画清信任边界再谈。
- 外部输入在系统边界校验(CLI 参数、API 路由、文件路径),校验完再用
- 数据库查询参数化,禁字符串拼接 SQL
- 输出编码防注入(命令与参数分离传参,不拼字符串执行)
- 跨信任边界的通信走 TLS/HTTPS;本机 IPC / Unix socket / 可信隔离网络等边界内传输按威胁模型判断,不因"内部网络"默认免认证、免加密
- 密码用慢哈希存哈希、禁明文;优先用平台提供的 password hashing API(如 ASP.NET Core
PasswordHasher,算法与参数由库负责),自实现时用 bcrypt/scrypt/argon2 类 - 发版前跑本仓包管理器原生审计,high+ 必须处置
- 路径用规范化(resolve/realpath 类)后断言仍在根内,拒空/截断符/超长/以
-开头,传参用数组 +--分隔
Ask First(需人批准)
- 新增/改动鉴权流程
- 存新类别敏感数据(PII、支付)
- 新增外部服务集成
- 改 CORS / 上传 / 限流
- 提权、改权限模型
Never(禁令)
- secrets 进代码、进 git、进日志(token、key、密码)
- 客户端校验当安全边界
eval/ shell 字符串拼接跑用户输入- 关安全头图方便;向用户暴露堆栈与内部错误细节
- 浏览器 Web 应用把高权限 session/token 放入可被不可信页面脚本直接读取的存储(localStorage/sessionStorage 等)——优先 HttpOnly + Secure + SameSite Cookie;桌面 / CLI / 移动端等非浏览器客户端按平台安全存储机制处理(DPAPI / Keychain / Keystore 等),不套用浏览器禁令
Secrets 分层
代码 ← 只读环境变量,无值即报错退出,不给默认值
本地 ← .env(gitignore)+ .env.example 占位
CI ← 平台 Secrets → 环境变量注入,不 echo 不落盘
生产 ← 部署平台/密钥管理,CI 永远不持生产密钥
泄露 ← 按已泄露处理:吊销重建 + 清 git 历史(filter-repo 类),光删除文件不够
注入与遍历
- 命令注入:
target="a;rm -rf /"、$(...)、反引号、&&|||——解法:命令与参数分离传参(参数数组/无 shell 模式),输入只当参数/工作目录,不进命令字符串 - 参数注入:
target="--upload-pack=..."以-开头被当 option——解法:拒-开头 +--分隔 - 路径遍历:
target="../../etc"+ 拼接写盘写出仓外——解法:规范化(resolve/realpath 类)后断言仍在根内,确认存在且是目录,非目标拒绝 - SQL 注入:参数化 / ORM,禁拼接
- XSS/输出注入:框架自动转义,不绕过;必须渲染 HTML 先 sanitize
依赖漏洞处置
audit 报 high+ → 先判可达性(走不走那条路径)
→ 可达:先临时缓解(限输入/关特性/pin+最小补丁)降暴露面,同时开分支试升级
→ 不可达:记录 + 跟踪,不无视
→ 压着不升 = 留雷;直接升 = 拿生产当测试;都不接受
Quick Reference
| 场景 | 动作 |
|---|---|
| 用户输入进命令 | 命令与参数分离传参,不拼字符串 |
| 路径输入 | 规范化→断言在根内→确认是目录 |
| token/secret | 环境变量 + Secrets,禁代码/日志/git |
| 发版前 | 原生审计跑一次,high+ 有处置记录 |
| 碰边界 | 先 STRIDE 5 分钟 + abuse case 当首测 |
| 改鉴权/加集成 | Ask First,先批再做 |
Common Rationalizations
| Excuse | Reality |
|---|---|
| "内部工具,不用校验输入" | 内部工具的调用方也是不可信输入;注入不分内外,边界即校验 |
| "先拼字符串,上线前再改参数数组" | 上线前即忘记时;拼字符串是 Never 级,出现即拦 |
| "这个漏洞我们走不到" | 走不到是结论不是感觉;拿出调用链证据,否则按可达处置 |
| "升级会 break,先压着" | 压着=留雷;先缓解降暴露面,再排期升级,两步都得有记录 |
| "日志打 token 方便排查" | 排查方便泄露也方便;脱敏/引 ID,值永不进日志 |
| "误提交了 git rm 就行" | 历史里还在;吊销重建 + filter-repo 清历史 |
Red Flags — STOP
- shell 字符串拼接用户输入(命令与变量拼成一个字符串执行)
- 路径未规范化即
join+write - secret 进代码/git/日志
- audit high+ 无处置记录
- 碰信任边界无 STRIDE / 无 abuse case
- 关安全检查图方便(headers、校验、CORS)
- 向用户吐堆栈与内部细节
以上任一出现 → 停手,修代码不修 bar。
Verification
- 威胁模型已画(边界/资产/STRIDE/abuse case)
- 输入在边界校验,命令用参数数组,路径规范化 + 根内断言
- secrets 全走环境变量/Secrets,无代码/日志/git 残留
- 发版审计已跑,high+ 有可达性结论 + 处置记录
- Ask First 项有人批准记录