You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
StructuredFormatter._format_json calls json.dumps without allow_nan=False. allow_nan defaults to True, so a non-finite float in performance_ms or correlation_id is emitted as a bare NaN / Infinity / -Infinity token.
Those tokens are not valid JSON. RFC 8259 has no numeric literal for them — they are a CPython extension. A strict parser rejects the record, which means the record is effectively lost at the consumer even though it was written to the sink intact.
Reproduction
Verified on origin/main @ 715cbf5 (i.e. this is pre-existing, not introduced by the #1452 follow-up).
json.loads accepts NaNby default, so every round-trip test written in Python passes and the record looks fine locally. It breaks in the consumer: Go's encoding/json, Jackson without ALLOW_NON_NUMERIC_NUMBERS, and most log pipelines reject it. JSON_LOGGING defaults to "true" in production_config.py:72, so the JSON path is the production path.
Reachability
Low severity, same shape as #1452. Not attacker-reachable, and not currently reachable at all:
correlation_id comes from record.request_id and from header values — strings.
performance_ms is built as a formatted string (f"{record.duration * 1000:.2f}ms"), so it is never a raw float today. Note float("nan") formats to the string "nanms" through that path, which is ugly but valid JSON.
Reaching this needs a future call site passing a non-finite float directly.
Like #1452, this is a latent gap plus a correctness claim worth closing while the surrounding code is fresh, not an incident.
Filing it once so it is tracked rather than re-derived — the waste #1452 itself documented.
Suggested fix
allow_nan=False makes json.dumps raise ValueError on a non-finite float, which #1488's tier-2 fallback then catches and degrades cleanly — so after #1488 lands, this is close to a one-word change plus a test. Doing it before #1488 would instead turn a malformed record into a lost one, so the ordering matters.
Acceptance criteria
A record with float("nan"), float("inf") or float("-inf") in an enrichment field parses under a strict parser.
A regression test asserts the emitted record contains no bare NaN/Infinity token, and fails on the pre-fix implementation.
The record is still emitted — a non-finite float must not become a dropped record.
Summary
StructuredFormatter._format_jsoncallsjson.dumpswithoutallow_nan=False.allow_nandefaults toTrue, so a non-finitefloatinperformance_msorcorrelation_idis emitted as a bareNaN/Infinity/-Infinitytoken.Those tokens are not valid JSON. RFC 8259 has no numeric literal for them — they are a CPython extension. A strict parser rejects the record, which means the record is effectively lost at the consumer even though it was written to the sink intact.
Reproduction
Verified on
origin/main@715cbf5(i.e. this is pre-existing, not introduced by the #1452 follow-up).Parsed with the constant hook disabled, which is what a strict parser does:
Why it hides
json.loadsacceptsNaNby default, so every round-trip test written in Python passes and the record looks fine locally. It breaks in the consumer: Go'sencoding/json, Jackson withoutALLOW_NON_NUMERIC_NUMBERS, and most log pipelines reject it.JSON_LOGGINGdefaults to"true"inproduction_config.py:72, so the JSON path is the production path.Reachability
Low severity, same shape as #1452. Not attacker-reachable, and not currently reachable at all:
correlation_idcomes fromrecord.request_idand from header values — strings.performance_msis built as a formatted string (f"{record.duration * 1000:.2f}ms"), so it is never a raw float today. Notefloat("nan")formats to the string"nanms"through that path, which is ugly but valid JSON.Like #1452, this is a latent gap plus a correctness claim worth closing while the surrounding code is fresh, not an incident.
Relationship to #1452 and #1488
Found during the step-5 red-team pass on #1488 (the #1452 follow-up). Filing separately rather than folding it in, for two reasons:
default=strin_format_jsondoes not deliver its stated guarantee — a record can still be lost (follow-up to #1439) #1452 is "the record never reaches the sink"; this is "the record reaches the sink but the consumer cannot parse it". fix(logging): never lose a JSON log record to a serialization error #1488's fallback tiers do not touch it, becauseNaNdoes not makejson.dumpsraise.PR Governancerequires exactly one closing reference per PR, and fix(logging): never lose a JSON log record to a serialization error #1488 already closesdefault=strin_format_jsondoes not deliver its stated guarantee — a record can still be lost (follow-up to #1439) #1452.Filing it once so it is tracked rather than re-derived — the waste #1452 itself documented.
Suggested fix
allow_nan=Falsemakesjson.dumpsraiseValueErroron a non-finite float, which #1488's tier-2 fallback then catches and degrades cleanly — so after #1488 lands, this is close to a one-word change plus a test. Doing it before #1488 would instead turn a malformed record into a lost one, so the ordering matters.Acceptance criteria
float("nan"),float("inf")orfloat("-inf")in an enrichment field parses under a strict parser.NaN/Infinitytoken, and fails on the pre-fix implementation.