加强错误报告凭据脱敏
Project CI / Repository checks (pull_request) Successful in 2m35s
Project CI / Backend tests (pull_request) Successful in 5m56s
Project CI / Frontend tests (pull_request) Successful in 3m20s
Project CI / Native shell tests (pull_request) Successful in 16m51s

使用 Unicode 空白归一化匹配凭据标记

补充 NBSP 和 EM SPACE 回归测试并同步技术文档
This commit is contained in:
2026-09-02 20:01:04 +08:00
parent 50d6245829
commit beda813a59
2 changed files with 21 additions and 4 deletions
@@ -19,7 +19,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
## HTTP 与存储
- 登录态客户端使用 `POST /api/error-reports`,请求 DTO 位于 `shared-contracts::error_reports`
- api-server 对请求体设置 24 MiB 上限,并校验 schemaVersion、submissionId、事件/日志数量和 20 MiB 压缩包上限;事件字段、用户说明和日志名/内容均做长度限制与凭据脱敏,归档使用 `events.jsonl`(每行一个事件)。结构化事件只保存在当前进程内,用户提交时才生成 `events.jsonl`,不在磁盘单独持久化。submissionId 提供重放幂等;客户端对同一批事件复用稳定 submissionId,服务端提交成功后立即反馈成功,本地事件 ack 独立重试,不因 ack 失败误报提交失败;SpacetimeDB procedure 使用服务 identity 门禁,`user_id` 由 api-server 注入并在 module 校验,时间戳仍使用 `ctx.timestamp`。创建 procedure 额外限制单 identity 每小时 100 次提交。
- api-server 对请求体设置 24 MiB 上限,并校验 schemaVersion、submissionId、事件/日志数量和 20 MiB 压缩包上限;事件字段、用户说明和日志名/内容均做长度限制与凭据脱敏(匹配归一化覆盖 Unicode 空白字符),归档使用 `events.jsonl`(每行一个事件)。结构化事件只保存在当前进程内,用户提交时才生成 `events.jsonl`,不在磁盘单独持久化。submissionId 提供重放幂等;客户端对同一批事件复用稳定 submissionId,服务端提交成功后立即反馈成功,本地事件 ack 独立重试,不因 ack 失败误报提交失败;SpacetimeDB procedure 使用服务 identity 门禁,`user_id` 由 api-server 注入并在 module 校验,时间戳仍使用 `ctx.timestamp`。创建 procedure 额外限制单 identity 每小时 100 次提交。
- 归档构建只在请求生命周期内使用受 20 MiB 上限约束的内存 `Vec<u8>`,随后直接 PUT 到私有 OSS;服务端不写本地报告文件,也不保留本地索引。OSS 上传失败不写入数据库,调用方可稍后重新提交。
- 归档对象使用固定私有 OSS key:`agc/error-reports/v1/{batchId}.zip`;key 只由报告 UUID 决定,不包含时间戳。上传成功后才写入 SpacetimeDB `error_report` 元数据表;`userId + submissionId` 由唯一幂等键保证重放返回已有记录。完整事件、说明和日志只存在 OSS ZIP。
- `agc` 是服务端专用私有前缀;公共直传票据、通用 object-key 规范化和 legacy 公开路径均拒绝该前缀。归档内同名日志会自动加数字后缀,读取本机诊断日志时拒绝符号链接/非普通文件。
@@ -645,10 +645,10 @@ fn sanitize_report_text(v: &str) -> String {
sanitize_report_text_with_limit(v, MAX_EVENT_FIELD_CHARS)
}
fn sanitize_report_text_with_limit(v: &str, max: usize) -> String {
let l = v.to_ascii_lowercase();
let normalized = l
let normalized = v
.to_lowercase()
.chars()
.filter(|c| !c.is_ascii_whitespace())
.filter(|c| !c.is_whitespace())
.collect::<String>();
if [
"authorization:",
@@ -679,6 +679,23 @@ fn sanitize_report_text_with_limit(v: &str, max: usize) -> String {
.take(max)
.collect()
}
#[cfg(test)]
mod tests {
use super::sanitize_report_text_with_limit;
#[test]
fn redacts_credentials_separated_by_unicode_whitespace() {
for value in [
"token\u{00a0}=\u{2003}secret",
"Authorization:\u{2003}Bearer\u{00a0}secret",
"api-key\u{00a0}:\u{2003}secret",
] {
assert_eq!(sanitize_report_text_with_limit(value, 512), "[REDACTED]");
}
}
}
fn sanitize_log_name(v: &str) -> String {
let s = v
.rsplit(['/', '\\'])