核心问题

如果用户或开发者什么都不配置,系统是否仍然安全?

安全默认值的原则是:

默认行为应该保护用户,而不是把风险留给用户或后来维护者。

很多事故不是因为系统没有安全能力,而是因为安全能力默认没开。

例如:

  • 默认公开
  • 默认允许所有来源访问
  • 默认管理员权限过大
  • 默认 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 检查清单

设计默认值时问:

  1. 什么都不配置时,系统是否安全?
  2. 默认是允许还是拒绝?
  3. 默认是公开还是私有?
  4. 失败时进入安全状态还是危险状态?
  5. token 默认是否短期、最小 scope、可撤销?
  6. 日志默认是否脱敏?
  7. 错误默认是否隐藏内部细节?
  8. 分享默认是否可控、可撤销、不过度公开?
  9. 高风险操作是否有适当摩擦?
  10. 用户是否能理解并改变默认值?

小结

  1. 安全不应该依赖记忆,应该依赖默认值。
  2. 默认拒绝比默认允许更安全。
  3. 公开应该是动作,不应该是意外。
  4. 高风险系统应该 fail closed。
  5. Token 是便携式权限,默认必须保守。
  6. 日志和错误默认不能泄露敏感信息。
  7. 分享链接默认应该可控、可撤销、不过度公开。
  8. 高风险操作需要保护性摩擦。