正文

日志能帮你定位线上问题,也能把线上问题扩大成数据泄露。接口报错时多打一行 request body,支付回调失败时把完整订单对象丢进日志,AI 应用调试时保存完整 prompt,看起来都方便排查;一旦日志进入云日志平台、CI 构建输出、客服工单、异常上报系统或第三方 APM,敏感数据就多了一条复制链路。

真实事故已经说明,问题不一定从数据库开始。

2018-05-03,Twitter 披露一个密码处理缺陷。Wired 报道称,Twitter 本来会用 bcrypt 处理密码,但缺陷导致密码在完成哈希前被记录到内部日志中,平台建议用户修改密码。这个案例提醒开发者:敏感数据只要在某个中间环节被日志捕获,后面的存储加密和权限控制都只能减轻影响,不能抹掉已经写出的明文。

2019-03-21,Wired 报道 Facebook 承认数亿用户密码曾以可读格式存在内部系统中。报道还提到,部分明文密码是通过 crash logs 等内部机制被意外捕获。对开发者来说,这比“某个地方忘了哈希”更值得警惕:异常日志、调试日志、网络日志和 crash dump 都可能无意中带上原始输入。

2023-03-24,OpenAI 公布 ChatGPT 3 月 20 日事故复盘:Redis 客户端 bug 让部分用户看到其他用户的对话标题,并可能让 1.2% 活跃 ChatGPT Plus 用户的部分支付相关信息短暂可见。OpenAI 同时说明,他们通过日志检查消息是否只对正确用户可见,并改进了日志以确认问题停止。这个案例不是“日志泄露”的简单版本,但它说明了同一件事:缓存、日志和调试数据都处在用户数据流上,记录什么、保留多久、谁能读,必须提前设计。

MITRE 把“敏感信息写入日志文件”归为 CWE-532。OWASP Logging Cheat Sheet 也明确建议不要直接记录 access token、session 标识、密码、数据库连接串、密钥、支付卡数据和敏感个人信息。日志不是越详细越好,日志应该足够排查问题,但不能成为第二份用户数据库。

上线前先查这五类风险:

风险常见写法更稳的记录方式
token / cookie打印完整 headers只记录 authType、token 指纹、校验结果
手机号 / 邮箱打印完整用户对象只记录脱敏值或 hash
订单 / 支付打印完整订单和回调 payload只记录订单号、金额、状态、渠道流水号
prompt / 对话保存完整用户输入和 system prompt只记录任务类型、模型、token 数、敏感分类
异常对象console.error(error) 带出请求上下文只记录错误类型、requestId、堆栈和脱敏字段

先把日志分成三种用途

不同用途的日志,能记录的字段不一样。

日志类型用来解决什么可以记录什么
诊断日志排查接口报错、性能慢、三方调用失败requestId、接口、状态码、耗时、错误类型
审计日志追踪谁在什么时候改了什么操作人 ID、动作、资源 ID、结果、IP 段
安全日志发现爆破、越权、异常访问登录失败、鉴权失败、限流命中、权限拒绝

不要用同一份“超详细业务日志”解决所有问题。诊断日志通常保留更短,访问人更少;审计日志要能证明事件发生过,但不应该保存完整请求体;安全日志要能触发告警,但不能把攻击 payload 原样无限期保存。

哪些字段不能直接进日志

上线前把这些字段当成禁止明文记录:

类型字段例子可替代记录
认证凭据Authorization、Cookie、refresh_token、api_key、session_idauth_present=true、token_hash=sha256前8位
密钥配置DATABASE_URL、PRIVATE_KEY、AWS_SECRET_ACCESS_KEY、NPM_TOKEN配置项名称、是否存在、来源环境
个人信息手机号、身份证、邮箱、地址、姓名脱敏手机号、用户 ID hash
支付信息银行卡、支付账号、完整回调、账单地址支付渠道、金额、状态、后四位按合规策略处理
订单详情商品明细、收货地址、优惠券、备注orderId、状态、金额区间、失败原因枚举
AI 输入完整 prompt、system prompt、上传文件正文、聊天记录任务类型、模型、输入长度、敏感分类结果

订单号本身不一定是秘密,但它可以成为查询入口。能不能记录完整订单号,要看系统里订单号是否可被外部枚举、客服系统是否能直接按订单号查详情、日志平台是否开放给外包或多团队。

先查有没有整包输出

最危险的日志通常不是手写 password=,而是整包输出。

先扫这些模式:

PowerShell
rg -n --hidden -g "!node_modules" -g "!dist" -g "!.output" "console\\.(log|info|warn|error)|logger\\.(debug|info|warn|error)|print\\(|System\\.out|log\\." .

再重点查 request、headers、body、user、order、prompt 这些对象名:

PowerShell
rg -n --hidden -g "!node_modules" -g "!dist" -g "!.output" "(req|request|headers|body|payload|user|order|payment|prompt|messages|token|cookie|authorization)" .

看到下面这类写法,先改掉:

TypeScript
logger.info({ headers: req.headers, body: req.body }, "request failed");
logger.error({ error, user, order }, "create order failed");
console.log("llm prompt", systemPrompt, userPrompt);

更稳的写法是只记录定位问题需要的字段:

TypeScript
logger.warn(
  {
    requestId,
    route: req.route?.path,
    method: req.method,
    statusCode: 400,
    userIdHash: hashUserId(user.id),
    orderId: order.id,
    errorCode: "ORDER_PRICE_CHANGED"
  },
  "create order rejected"
);

通过标准:日志能让你按 requestId、userIdHash、orderId、errorCode 找到问题,但看不到完整 token、cookie、手机号、地址、prompt 和支付回调原文。

headers 只保留允许清单

很多框架和网关会把请求头打进访问日志。请求头里最容易混进敏感数据:

  • Authorization
  • Cookie
  • Set-Cookie
  • X-Api-Key
  • X-Access-Token
  • X-Refresh-Token
  • 内部网关签名头

不要做“黑名单删除几个字段”后就放心。更稳的方式是允许清单:

TypeScript
const safeHeaders = {
  "user-agent": req.headers["user-agent"],
  "content-type": req.headers["content-type"],
  "x-request-id": req.headers["x-request-id"],
  "x-forwarded-for": maskIp(req.headers["x-forwarded-for"])
};

logger.info({ requestId, headers: safeHeaders }, "request metadata");

如果必须确认 token 变化,只记录不可逆指纹:

TypeScript
logger.info(
  {
    requestId,
    authType: "bearer",
    tokenFingerprint: fingerprintToken(req.headers.authorization)
  },
  "auth checked"
);

指纹只用于关联同一个 token 是否反复失败,不用于恢复 token。不要记录 token 前缀,因为很多平台的 token 前缀本身包含供应商、权限或账号信息。

手机号和邮箱要统一脱敏

不要在每个接口里临时写一套脱敏逻辑。日志脱敏应该走统一函数,并且有测试。

TypeScript
export function maskPhone(value: string) {
  return value.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
}

export function maskEmail(value: string) {
  const [name, domain] = value.split("@");
  if (!name || !domain) return "***";
  return `${name.slice(0, 2)}***@${domain}`;
}

日志里只使用脱敏后的值:

TypeScript
logger.info(
  {
    requestId,
    userIdHash: hashUserId(user.id),
    phone: maskPhone(user.phone),
    email: maskEmail(user.email)
  },
  "profile update requested"
);

通过标准:开发者想打印手机号或邮箱时,只能调用统一脱敏函数;代码评审里看到完整 user.phone、user.email 进入日志,要退回修改。

订单日志只记录排查所需字段

订单日志最容易被写得过细,因为订单问题需要排查库存、价格、优惠、支付、地址和配送。但完整订单对象通常包含:

  • 用户姓名、手机号、收货地址。
  • 商品明细、优惠券、备注。
  • 支付渠道返回的原始 payload。
  • 内部成本价、结算规则、风控结果。

订单失败日志可以这样记:

TypeScript
logger.warn(
  {
    requestId,
    orderId,
    userIdHash,
    amount,
    currency,
    status: "PAYMENT_FAILED",
    paymentProvider: "stripe",
    providerCode: "card_declined"
  },
  "payment failed"
);

不要这样记:

TypeScript
logger.warn({ order, paymentCallback, user }, "payment failed");

如果客服或运营需要看完整订单详情,让他们走受控后台和权限审计,不要让日志平台承担业务查询功能。

AI prompt 默认按敏感输入处理

AI 应用常见的调试日志会记录完整 prompt:

TypeScript
logger.debug({ systemPrompt, userPrompt, messages }, "llm request");

这类日志可能包含用户上传的合同、简历、源代码、数据库字段、客户名单、内部规则、system prompt 和工具调用结果。即使没有传统意义上的手机号和 token,prompt 也可能是敏感数据。

更稳的日志结构:

TypeScript
logger.info(
  {
    requestId,
    model,
    taskType: "support_summary",
    inputTokens,
    outputTokens,
    promptTemplateVersion,
    sensitiveClass: "customer_support",
    toolCallCount
  },
  "llm request completed"
);

确实需要保存 prompt 做质量复盘时,先满足这些条件:

条件要求
用户授权隐私政策或企业协议允许保存
数据脱敏token、手机号、邮箱、地址、订单和文件正文已处理
存储隔离不和普通应用日志混在一起
访问控制只有需要复盘的人能读
保留期限到期自动删除

没有这些条件,就不要把 prompt 当普通 debug 字符串打印。

异常日志要去掉原始上下文

很多异常对象会带上请求配置、响应体、headers 和重试上下文。例如 HTTP 客户端、数据库 SDK、支付 SDK、AI SDK 报错时,error 里可能包含完整请求。

先把异常转成安全结构:

TypeScript
function toSafeError(error: unknown) {
  if (!(error instanceof Error)) {
    return { name: "UnknownError", message: "Unknown error" };
  }

  return {
    name: error.name,
    message: error.message,
    stack: error.stack?.split("\n").slice(0, 8).join("\n")
  };
}

logger.error(
  {
    requestId,
    error: toSafeError(error)
  },
  "third party request failed"
);

如果错误消息本身可能包含 SQL、URL、token 或请求体,要在日志库的 formatter 里做二次清洗。不要只相信业务代码每次都会手动脱敏。

日志库要配置全局 redaction

只靠开发者自觉不够。日志库、网关、APM 和异常上报都要有统一脱敏层。

以常见 Node.js 日志结构为例,可以把敏感路径放到全局 redaction 里:

TypeScript
const redactPaths = [
  "req.headers.authorization",
  "req.headers.cookie",
  "headers.authorization",
  "headers.cookie",
  "body.password",
  "body.token",
  "body.refresh_token",
  "body.api_key",
  "user.phone",
  "user.email",
  "payment.card",
  "messages",
  "prompt",
  "systemPrompt"
];

生产环境的目标不是“开发者不犯错”,而是开发者偶尔打错日志时,敏感字段仍然被统一拦住。

日志平台权限要比业务后台更严格

日志平台通常聚合了多个系统的数据,权限不能按“开发都能看”处理。

上线前检查这些点:

检查项通过标准
访问权限只有排障、SRE、安全和值班人员能读生产日志
查询审计谁查了哪个时间段、哪个用户、哪个字段可追踪
导出控制大批量导出需要审批或禁止
保留期限debug 日志短保留,审计日志按合规要求保留
环境隔离测试、预发、生产日志不混放
第三方同步APM、错误上报、客服系统只接收脱敏字段

如果日志平台能按手机号、邮箱、订单号直接查完整用户行为,它已经接近业务数据库,权限和审计也要按业务数据库处理。

上线前跑一张日志检查表

发布前至少确认这些项:

Text
- [ ] No full Authorization, Cookie, Set-Cookie, API key, refresh token in logs
- [ ] No full phone, email, ID number, address, payment account in logs
- [ ] No full request body or response body in normal logs
- [ ] No full order object, payment callback payload, shipping address in logs
- [ ] No full AI prompt, system prompt, messages, uploaded file text in logs
- [ ] Logger redaction is configured globally
- [ ] Error objects are converted to safe fields before logging
- [ ] Debug logging is disabled or tightly scoped in production
- [ ] Production log access is limited and audited
- [ ] Log retention matches data sensitivity
- [ ] CI and deployment logs do not print secrets or environment variables
- [ ] Security events still keep requestId, actor, action, target and result

再用几条搜索命令检查构建产物、配置和脚本:

PowerShell
rg -n --hidden -g "!node_modules" -g "!dist" -g "!.output" "(Authorization|Set-Cookie|refresh_token|api_key|secret|password|private_key|DATABASE_URL)" .
PowerShell
rg -n --hidden -g "!node_modules" -g "!dist" -g "!.output" "(systemPrompt|userPrompt|messages|prompt|paymentCallback|request\\.body|req\\.body|req\\.headers)" .

这些命令不会证明日志绝对安全,但能抓出最常见的危险写法。真正上线时,还要结合代码评审、日志库 redaction、APM 配置、网关访问日志和生产权限一起看。

已经打进日志怎么办

发现敏感数据已经进入日志后,不要只删一行代码。

按泄露处理:

  1. 先停掉继续写入的日志语句或调低生产日志级别。
  2. 确认数据进入了哪些位置:应用日志、网关日志、APM、错误上报、CI 输出、客服工单、对象存储。
  3. 删除或隔离包含敏感数据的日志副本,并记录处理范围。
  4. 如果包含 token、cookie、API key、refresh token、数据库密码或私钥,立即轮换。
  5. 如果包含手机号、地址、支付信息、聊天内容或 prompt,按隐私和合规流程评估通知、留痕和后续处理。
  6. 给日志库补全局 redaction,并补测试,避免同类字段再次进入日志。

日志的价值是让你知道系统发生了什么,不是保存系统看到过的一切。上线前把“能不能排查问题”和“会不会泄露数据”同时校验,日志才是工程资产,而不是下一次事故的入口。

参考来源

Change Your Twitter Password Right NowWiredFacebook Stored Millions of Passwords in PlaintextWiredMarch 20 ChatGPT outage: Here's what happenedOpenAILogging Cheat SheetOWASP Cheat Sheet SeriesCWE-532: Insertion of Sensitive Information into Log FileMITRE CWESecret scanning patternsGitHub Docs

相关文章

API 接口上线前要查什么:鉴权、越权、限流、幂等和错误返回开发环境 / 约 13 分钟Docker 镜像里最容易泄露什么:.env、构建缓存、SSH key 和多阶段构建开发环境 / 约 12 分钟npm 供应链攻击后,开发者要怎么检查依赖、lockfile 和 CI 密钥开发环境 / 约 13 分钟Vibe Coding 小应用上线前要查哪些安全坑智能编程 / 约 12 分钟AI Agent 权限怎么设:哪些命令能自动跑,哪些必须人工确认智能编程 / 约 13 分钟7 个关键洞察:AI Coding 工具真正改变的不是写代码,而是验证代码智能编程 / 约 18 分钟AI Coding 的下一步:从 prompt 技巧到工程约束智能编程 / 约 18 分钟装过 codexui-android 后,Codex 登录凭据可能泄露怎么办错误日志 / 约 8 分钟

作者信息