核心问题
如果用户或开发者什么都不配置,系统是否仍然安全?
安全默认值的原则是:
默认行为应该保护用户,而不是把风险留给用户或后来维护者。
很多事故不是因为系统没有安全能力,而是因为安全能力默认没开。
例如:
- 默认公开
- 默认允许所有来源访问
- 默认管理员权限过大
- 默认 token 永不过期
- 默认日志记录敏感信息
- 默认不校验 webhook 签名
- 默认关闭审计
- 默认错误信息暴露内部细节
安全如果依赖每个人都记得正确配置,就迟早会失败。
核心句:
安全不应该依赖记忆,应该依赖默认值。
Default Deny
最重要的安全默认值是:
默认拒绝。
权限系统应该默认:
没有明确允许,就拒绝。
防火墙、API、数据访问、后台功能都一样。
坏默认:
function canAccessCourse(accountId, courseId) {
const rule = findAccessRule(accountId, courseId)
if (!rule) return true
return rule.allowed
}
好默认:
function canAccessCourse(accountId, courseId) {
const rule = findAccessRule(accountId, courseId)
if (!rule) return false
return rule.allowed
}
拒绝路径应该是系统的自然状态,不是异常状态。
Default Private
用户数据和内容应该默认私有,除非用户明确选择公开。
例如:
- 个人资料默认不公开
- 学习记录默认不公开
- 组织成员列表默认不公开
- 课程草稿默认不公开
- 上传文件默认不公开
- API key 默认不展示完整值
坏默认:
新建课程后立即公开。
好默认:
新建课程是 draft,必须显式 publish。
核心句:
公开应该是动作,不应该是意外。
Safe Failure
系统失败时,应该进入安全状态。
例如权限服务不可用时:
坏做法:
try {
return await permissionService.canEditCourse(accountId, courseId)
} catch {
return true
}
好做法:
try {
return await permissionService.canEditCourse(accountId, courseId)
} catch {
return false
}
当然,这可能影响可用性。
所以要按场景权衡:
- 支付、权限、安全:失败时拒绝
- 低风险推荐、展示:失败时降级
- 日志、分析:失败时不阻塞主流程
原则:
高风险系统应该 fail closed,低风险体验可以 fail open 或 degrade gracefully。
Secure Token Defaults
Token 默认值尤其重要。
坏默认:
- 永不过期
- 可以访问所有资源
- 明文存储
- URL 中传递
- 泄露后无法撤销
- scope 不清楚
好默认:
- 短期过期
- 明确 scope
- 可撤销
- 只显示一次
- 存 hash
- 高风险操作需要重新认证
- refresh token 可轮换
例如:
API token 默认只读。
需要写权限必须显式申请。
导出数据需要额外 scope。
核心句:
Token 是便携式权限,默认必须保守。
Safe Logging Defaults
日志默认不应包含敏感数据。
坏默认:
logger.info("request", { body: req.body, headers: req.headers })
这可能记录:
- password
- token
- cookie
- payment data
- private messages
- PII
好默认:
logger.info("request", {
requestId,
route,
accountId,
statusCode,
durationMs,
})
敏感字段要默认 redaction:
password
token
authorization
cookie
cardNumber
ssn
原则:
日志默认应该可用于排查,而不是变成数据泄露副本。
Safe Error Defaults
错误信息也有默认值。
坏错误:
{
"error": "SQL error: relation users_passwords does not exist"
}
这暴露内部结构。
好错误:
{
"code": "INTERNAL_ERROR",
"message": "Something went wrong.",
"requestId": "req_123"
}
内部日志可以记录详细错误。
对外响应应该避免暴露:
- SQL
- stack trace
- filesystem path
- internal service names
- token
- secret
- implementation detail
Safe Sharing Defaults
分享功能要特别注意默认值。
例如:
生成分享链接
要问:
- 链接是否公开可访问?
- 是否需要登录?
- 是否有过期时间?
- 是否可撤销?
- 是否允许搜索引擎索引?
- 是否包含敏感信息?
好默认:
- 默认需要登录
- 默认最小可见范围
- 默认可撤销
- 默认有过期时间
- 默认不被搜索引擎索引
Dangerous Operations Need Friction
安全默认值不是让所有操作都无摩擦。
高风险操作应该默认有摩擦:
- 删除账号
- 删除组织
- 导出用户数据
- 修改权限
- 关闭 MFA
- 生成高权限 token
- 大额退款
- 永久封号
常见安全摩擦:
- 二次确认
- 输入资源名确认
- 重新认证
- 审批
- 冷却时间
- 邮件通知
- 审计日志
这里的摩擦是在保护用户和系统,不是 dark pattern。
判断标准:
摩擦是为了防误操作和滥用,还是为了阻碍用户维护自身利益?
Secure Defaults 检查清单
设计默认值时问:
- 什么都不配置时,系统是否安全?
- 默认是允许还是拒绝?
- 默认是公开还是私有?
- 失败时进入安全状态还是危险状态?
- token 默认是否短期、最小 scope、可撤销?
- 日志默认是否脱敏?
- 错误默认是否隐藏内部细节?
- 分享默认是否可控、可撤销、不过度公开?
- 高风险操作是否有适当摩擦?
- 用户是否能理解并改变默认值?
小结
- 安全不应该依赖记忆,应该依赖默认值。
- 默认拒绝比默认允许更安全。
- 公开应该是动作,不应该是意外。
- 高风险系统应该 fail closed。
- Token 是便携式权限,默认必须保守。
- 日志和错误默认不能泄露敏感信息。
- 分享链接默认应该可控、可撤销、不过度公开。
- 高风险操作需要保护性摩擦。