正文

日志能帮你定位线上问题,也能把线上问题扩大成数据泄露。接口报错时多打一行 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
手机号 / 邮箱打印完整用户对象
订单 / 支付打印完整订单和回调 payload
prompt / 对话保存完整用户输入和 system prompt
异常对象console.error(error) 带出请求上下文

先把日志分成三种用途

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

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

诊断日志排查接口报错、性能慢、三方调用失败
审计日志追踪谁在什么时候改了什么
安全日志发现爆破、越权、异常访问

哪些字段不能直接进日志

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

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

认证凭据AuthorizationCookierefresh_tokenapi_keysession_id
密钥配置DATABASE_URLPRIVATE_KEYAWS_SECRET_ACCESS_KEYNPM_TOKEN
个人信息手机号、身份证、邮箱、地址、姓名
支付信息银行卡、支付账号、完整回调、账单地址
订单详情商品明细、收货地址、优惠券、备注
AI 输入完整 prompt、system prompt、上传文件正文、聊天记录

先查有没有整包输出

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

先扫这些模式:

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

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

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

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

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\\." .
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"
);

headers 只保留允许清单

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

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

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

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

  • 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");
TypeScript
logger.info(
  {
    requestId,
    authType: "bearer",
    tokenFingerprint: fingerprintToken(req.headers.authorization)
  },
  "auth checked"
);

手机号和邮箱要统一脱敏

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

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

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

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"
);

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

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

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

不要这样记:

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

  • 用户姓名、手机号、收货地址。
  • 商品明细、优惠券、备注。
  • 支付渠道返回的原始 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:

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

更稳的日志结构:

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

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

用户授权隐私政策或企业协议允许保存
数据脱敏token、手机号、邮箱、地址、订单和文件正文已处理
存储隔离不和普通应用日志混在一起
访问控制只有需要复盘的人能读
保留期限到期自动删除
TypeScript
logger.debug({ systemPrompt, userPrompt, messages }, "llm request");
TypeScript
logger.info(
  {
    requestId,
    model,
    taskType: "support_summary",
    inputTokens,
    outputTokens,
    promptTemplateVersion,
    sensitiveClass: "customer_support",
    toolCallCount
  },
  "llm request completed"
);

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

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

先把异常转成安全结构:

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

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"
);

日志库要配置全局 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、错误上报、客服系统只接收脱敏字段

上线前跑一张日志检查表

发布前至少确认这些项:

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

这些命令不会证明日志绝对安全,但能抓出最常见的危险写法。真正上线时,还要结合代码评审、日志库 redaction、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)" .

已经打进日志怎么办

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

按泄露处理:

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

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

作者信息