AI辅助整理;Python 3.11标准库示例和测试在本地离线执行。所有敏感标记均为虚构,没有模型请求,也未读取生产日志。
排错日志里出现密钥,常见修补方法是增加一条替换规则。但新的异常正文、嵌套headers或伪造换行一来,替换表还要继续补。
本文实现另一个边界:日志事件只接收六个固定字段,原始请求、原始异常和任意自由文本都不进入事件。结果是一个可以json.loads还原的单行JSON。完整实现、测试和执行输出都在文中,不依赖外部下载。
1. 输入输出及信任范围
| 字段 | 本例政策 | 取值来源 |
|---|---|---|
| request_id | 小写规范形式的UUIDv4 | 应用生成,不能由用户代填 |
| stage | prepare/transport/parse | 应用控制流 |
| outcome | ok/failed/unknown | 应用观察到的结果 |
| error_code | none/timeout/http_error/invalid_response/internal | 内部固定映射 |
| http_status | None或100—599整数,拒绝布尔值 | 已核对的状态信息 |
| elapsed_ms | 0—86400000整数,拒绝布尔值 | 应用计时后换算 |
86400000是本例本地策略的一天毫秒数,不是接口超时上限。字段名、枚举、状态范围和UUID政策也是教学选择。对于耗时超过策略的任务,应修改有依据的业务政策并补测试,不能为了凑过校验而随意截断。
UUID校验只能检查格式,无法证明来源。调用方仍必须自己生成ID,例如使用标准库uuid.uuid4,而不是接受用户伪装成UUID的数据。这里验证HTTP状态类型和范围,没有实现各阶段、HTTP状态与业务成功之间的完整语义矩阵。
本例针对受控调用点构造的小字典,不是任意巨大JSON的网络入口。在读取外部请求之前仍应限制整体大小。本例不修改logging全局配置,不处理第三方SDK、代理或异常平台已写出的日志,也不保证远端收集器已保存事件。
2. 文件一:safe_event.py
Python 3.11,仅标准库。固定错误码不包含原始字段名或值;拒绝非精确内建类型,避免在校验过程中调用不受控对象的字符串转换。
"""An allowlisted telemetry boundary, not a general-purpose log scrubber."""
import json
from uuid import UUID
FIELDS = {"request_id", "stage", "outcome", "error_code", "http_status", "elapsed_ms"}
STAGES = {"prepare", "transport", "parse"}
OUTCOMES = {"ok", "failed", "unknown"}
ERRORS = {"none", "timeout", "http_error", "invalid_response", "internal"}
def encode_event(data):
# Exact builtins avoid implicit conversions and custom mapping callbacks.
if type(data) is not dict or len(data) != len(FIELDS):
raise ValueError("invalid_event_shape")
if any(type(key) is not str for key in data) or set(data) != FIELDS:
raise ValueError("invalid_event_shape")
request_id = data["request_id"]
if type(request_id) is not str or len(request_id) != 36:
raise ValueError("invalid_request_id")
try:
parsed = UUID(request_id)
except ValueError:
raise ValueError("invalid_request_id") from None
if str(parsed) != request_id or parsed.version != 4 or parsed.variant != "specified in RFC 4122":
raise ValueError("invalid_request_id")
for key, allowed in (("stage", STAGES), ("outcome", OUTCOMES), ("error_code", ERRORS)):
if type(data[key]) is not str or data[key] not in allowed:
raise ValueError("invalid_event_enum")
status = data["http_status"]
if status is not None and (type(status) is not int or not 100 <= status <= 599):
raise ValueError("invalid_http_status")
elapsed = data["elapsed_ms"]
if type(elapsed) is not int or not 0 <= elapsed <= 86400000:
raise ValueError("invalid_elapsed_ms")
# Both success and failure must have a consistent local classification.
if (data["outcome"] == "ok") != (data["error_code"] == "none"):
raise ValueError("inconsistent_outcome")
checked = {key: data[key] for key in sorted(FIELDS)}
return json.dumps(checked, ensure_ascii=True, allow_nan=False, separators=(",", ":")) + "\n"
if __name__ == "__main__":
sample = {"request_id": "12345678-1234-4234-8234-123456789abc", "stage": "transport",
"outcome": "unknown", "error_code": "timeout", "http_status": None, "elapsed_ms": 1200}
print(encode_event(sample), end="")
try:
encode_event(dict(sample, headers={"Authorization": "SYNTHETIC_SECRET"}))
except ValueError as exc:
print(str(exc))
先检查字典与字段数量,再检查键的类型及允许集合,避免把未知键和值拼进错误信息。枚举只接受固定字符串;耗时拒绝float,包括NaN和Infinity;状态码特别排除bool,因为Python中bool是int的子类。
序列化只发生在所有检查通过之后。ensure_ascii=True、紧凑JSON和唯一的末尾换行让输出格式明确;真正阻止换行伪造进入本例字段的,是严格取值约束。JSON编码本身不能代替敏感字段政策。
3. 运行及实际输出
执行python safe_event.py。本地实际输出:
{"elapsed_ms":1200,"error_code":"timeout","http_status":null,"outcome":"unknown","request_id":"12345678-1234-4234-8234-123456789abc","stage":"transport"}
invalid_event_shape
第一行只含六个字段。第二个输入故意加入headers,整条被拒绝;没有“只要日志失败就打印原始输入”的回退。实际接入时,捕获这里已知的校验异常,用固定计数器记录拒绝次数;不要把输入字典或任意异常字符串发到另一个日志出口。
unknown表示结果未知。比如进入发送边界后超时,不能把它解释成服务端没有处理或没有产生费用。http_status=None表示没有可确认的HTTP状态,并非0或200。
4. 文件二:test_safe_event.py
把测试与safe_event.py放到同一目录:
import io
import json
import unittest
from safe_event import encode_event
class Bomb:
def __str__(self):
raise AssertionError("custom stringification must never run")
class SafeEventTests(unittest.TestCase):
def setUp(self):
self.sample = {"request_id": "12345678-1234-4234-8234-123456789abc", "stage": "transport",
"outcome": "unknown", "error_code": "timeout", "http_status": None, "elapsed_ms": 1200}
def reject(self, data, code=None):
with self.assertRaises(ValueError) as caught:
encode_event(data)
if code:
self.assertEqual(str(caught.exception), code)
return str(caught.exception)
def test_roundtrip_single_line(self):
line = encode_event(self.sample)
self.assertEqual(line.count("\n"), 1)
self.assertEqual(json.loads(line), self.sample)
def test_unknown_fields_never_echo(self):
for key in ("headers", "prompt", "exception", "response_body", "SYNTHETIC_SECRET"):
with self.subTest(key=key):
error = self.reject(dict(self.sample, **{key: {"secret": "SYNTHETIC_SECRET"}}))
self.assertNotIn("SYNTHETIC_SECRET", error)
def test_missing_field(self):
for key in self.sample:
value = self.sample.copy(); del value[key]
self.reject(value, "invalid_event_shape")
def test_non_dict_and_subclass(self):
class Mapping(dict):
pass
for value in (None, [], Mapping(self.sample)):
self.reject(value, "invalid_event_shape")
def test_key_types_and_unicode_lookalike(self):
for key in (42, "http_statu\u0455", "stage\r\nforged"):
value = self.sample.copy(); del value["stage"]; value[key] = "transport"
self.reject(value, "invalid_event_shape")
def test_enum_rejects_injection(self):
for text in ("timeout\r\nforged=ok", "SYNTHETIC_SECRET", "超时", "timeout\x00", "x" * 10000):
self.reject(dict(self.sample, error_code=text), "invalid_event_enum")
def test_no_stringification(self):
for key in self.sample:
self.reject(dict(self.sample, **{key: Bomb()}))
def test_uuid_is_canonical_version_four(self):
for text in ("bad", self.sample["request_id"].upper(), "12345678-1234-1234-8234-123456789abc",
"12345678-1234-4234-0234-123456789abc", "x" * 100000):
self.reject(dict(self.sample, request_id=text), "invalid_request_id")
def test_http_status_types_and_bounds(self):
for status in (True, False, 200.0, "200", 99, 600):
self.reject(dict(self.sample, http_status=status), "invalid_http_status")
for status in (None, 100, 200, 599):
self.assertEqual(json.loads(encode_event(dict(self.sample, http_status=status)))["http_status"], status)
def test_duration_types_and_bounds(self):
for elapsed in (True, -1, 86400001, 0.1, float("nan"), float("inf")):
self.reject(dict(self.sample, elapsed_ms=elapsed), "invalid_elapsed_ms")
for elapsed in (0, 86400000):
self.assertEqual(json.loads(encode_event(dict(self.sample, elapsed_ms=elapsed)))["elapsed_ms"], elapsed)
def test_outcome_consistency(self):
self.reject(dict(self.sample, outcome="ok"), "inconsistent_outcome")
self.reject(dict(self.sample, error_code="none"), "inconsistent_outcome")
self.assertEqual(json.loads(encode_event(dict(self.sample, outcome="ok", error_code="none")))["outcome"], "ok")
def test_does_not_mutate_input(self):
before = self.sample.copy(); encode_event(self.sample)
self.assertEqual(self.sample, before)
def test_deterministic_order(self):
self.assertEqual(encode_event(self.sample), encode_event(dict(reversed(list(self.sample.items())))))
def test_sink_failure_is_caller_boundary(self):
sink = io.StringIO(); sink.close()
with self.assertRaises(ValueError):
sink.write(encode_event(self.sample))
if __name__ == "__main__":
unittest.main()
运行python -m unittest -v test_safe_event。这份文件14个测试方法通过,覆盖未知/缺失字段、嵌套敏感标记、不受控对象、伪造换行、Unicode相似键、UUID格式、bool与int边界、非有限数值、上界、输入不变和输出顺序。另有独立状态示例3个测试方法,本轮共17个方法通过。
这些是离线回归结果,不是渗透测试报告,也不证明部署后的全部日志没有泄漏。SYNTHETIC_SECRET只用来观察虚构标记是否出现在错误文本中,不是真实密钥。
5. 怎样接入,才不会在边界旁边开洞
应用进入请求处理时生成关联ID,并在控制流中更新阶段。发送层返回可确认的状态后,先映射到内部固定错误码,再构造六字段字典。encode_event返回字符串,由调用方决定写到什么接收端;成功生成字符串不等于写入成功。
写入失败是另一个边界。测试用已关闭的StringIO展示这个失败会抛出异常。生产项目应有符合业务要求的丢失计数、告警、容量和背压政策,不能为避免错误而无限排队,也不能用print(raw_request)兜底。本文没有实现异步队列、磁盘轮转或远端发送。
另外核查HTTP调试、代理访问日志、异常平台和链路追踪是否独立采集原文。不要假定自己的序列化函数可以管到这些出口。Pythonlogging参考说明exc_info会加入异常信息;在未知异常里直接附加原始请求对象,会绕过本文字段边界。
6. 常见错误与处理
- invalid_event_shape:检查调用点是否擅自加了prompt、headers、exception或response_body。需要新字段时先说明用途、数据等级与允许值,补测试后再扩展。
- invalid_request_id:检查是否由应用生成、是否是规范小写UUIDv4。不要把任意外部请求头当作可信关联ID。
- invalid_event_enum:在发送层映射内部错误码;不要把原始异常内容直接塞进error_code。
- invalid_elapsed_ms:统一计时单位,明确取整政策。本例仅接收已处理好的整数,不会替调用方转换字符串或浮点值。
- inconsistent_outcome:ok必须对应none,failed/unknown必须对应非none。更复杂的跨字段约束需由项目补充。
- 收集器没有记录:先检查接收端返回与容量;编码器通过不证明传输、落盘或保留策略已生效。
7. 练习、核验与下一篇
给本例增加一个确有排错用途的字段,例如固定的operation枚举。先写一个未知operation值必须拒绝的测试,再实现字段政策;不要开放任意字符串。用虚构标记检查错误信息和输出都不含新增的原始文本。
检查清单:字段目的明确;ID由应用生成;没有请求/响应原文;错误码不回显输入;换行和类型边界有样本;写入失败有处理;外围出口、访问权限和保留期限另行核查。
OWASP日志指南提供敏感数据排除和日志注入防护的原则;本文代码是面向这些问题的教学实现,不是该指南的官方参考实现。2026年10月29日已筛选近期官方动态,本课采用常青工程教学,没有借公告宣称本站或SDK增加新能力。
下一篇用本地模拟请求检查异步客户端与资源关闭。欢迎在评论区提供一个虚构的日志边界样本,勿贴生产日志、用户资料或密钥。
本文介绍基于Python 3.11标准库的六字段安全日志事件实现,严格限定输入为固定字段(如request_id、error_code、http_status等),拒绝原始请求、异常及自由文本,确保输出为可解析的单行JSON。包含完整校验逻辑、17项离线单元测试、UUID与状态码边界处理、类型安全约束及接入防洞指南,强调日志注入防护与敏感数据排除。
435

被折叠的 条评论
为什么被折叠?



