{"repro_id":"REPRO-2026-00286","version":7,"title":"Log4j MapMessage emits invalid JSON for non-finite values","repro_type":"security","status":"published","severity":"medium","cvss_score":6.3,"description":"MapMessage.asJson() in Apache Log4j API emits bare NaN, Infinity, or -Infinity tokens when a logged MapMessage contains non-finite IEEE 754 floating-point values. These bare tokens violate RFC 8259 and produce invalid JSON that downstream parsers reject. This is a bypass of the incomplete fix for CVE-2026-34481, which fixed JsonTemplateLayout's JsonWriter but did not cover the MapMessage.asJson() / getFormattedMessage([\"JSON\"]) code path in log4j-api.","root_cause":"# RCA Report — CVE-2026-49844\n\n## Summary\n\nCVE-2026-49844 is an improper encoding/serialization flaw in Apache Log4j API's `MapMessage` JSON output. When a `MapMessage` containing non-finite floating-point values (`NaN`, `Infinity`, `-Infinity`) is serialized to JSON via `MapMessage.asJson()` or `MapMessage.getFormattedMessage(new String[]{\"JSON\"})`, the `MapMessageJsonFormatter` emits the bare tokens `NaN`, `Infinity`, and `-Infinity` instead of an RFC 8259-compliant representation. These bare tokens are not valid JSON values, so any conformant downstream JSON parser or log-ingestion system will reject or fail to process the affected log records. This is a bypass of the earlier CVE-2026-34481 fix (which addressed `JsonTemplateLayout` but did not cover the `MapMessage.asJson()` code path in `log4j-api`).\n\n## Impact\n\n- **Package/component affected:** `org.apache.logging.log4j:log4j-api` — class `org.apache.logging.log4j.message.MapMessageJsonFormatter`, reached via `MapMessage.asJson(StringBuilder)` and `MapMessage.getFormattedMessage(String[])` with the `\"JSON\"` format.\n- **Affected versions:** `log4j-api` 2.13.1 through 2.25.4, and 2.26.0. (Also `3.0.0-alpha1`+ per the Snyk advisory.)\n- **Fixed versions:** 2.25.5 and 2.26.1.\n- **Risk level:** Medium (CWE-116: Improper Encoding or Escaping of Output).\n- **Consequences:** Malformed JSON in log output. Downstream JSON parsers and log-ingestion/indexing pipelines that enforce RFC 8259 will reject or fail on affected records. An attacker who can influence a floating-point value logged in a `MapMessage` (e.g., via `JsonTemplateLayout`'s message resolver or any layout relying on `MapMessage.asJson()`) can inject non-finite values to corrupt log records or disrupt log processing. This is not a remote code execution issue.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Improper encoding of non-finite floating-point values during `MapMessage` JSON serialization producing output that is not valid JSON (serialization/encoding flaw, `other`). Explicitly stated as not an RCE issue.\n- **Reproduced impact from this run:** All 13 test cases (NaN, +Infinity, -Infinity as scalar `Double`, `double[]`, scalar `Float`, `float[]`, plus a combined `asJson()` direct call) produce bare `NaN`/`Infinity`/`-Infinity` tokens in the vulnerable version. A strict RFC 8259 JSON parser (Gson `JsonReader` with `lenient=false`) rejects all 13 outputs. The fixed version quotes all values as JSON strings (`\"NaN\"`, `\"Infinity\"`, `\"-Infinity\"`) and all 13 parse successfully.\n- **Parity:** `full` — the reproduced behavior exactly matches the disclosed impact (invalid JSON from non-finite values; fix quotes them).\n- **Not demonstrated:** N/A — no code execution was claimed or expected.\n\n## Root Cause\n\nIn the vulnerable `MapMessageJsonFormatter.java` (log4j-api ≤ 2.25.4 / 2.26.0), the `formatNumber()` method handles `Double` and `Float` values by directly appending the primitive to the `StringBuilder`:\n\n```java\n// VULNERABLE (rel/2.25.4)\n} else if (number instanceof Double) {\n    final double doubleNumber = (Double) number;\n    sb.append(doubleNumber);          // <-- produces \"NaN\", \"Infinity\", \"-Infinity\"\n} else if (number instanceof Float) {\n    final float floatNumber = (float) number;\n    sb.append(floatNumber);           // <-- produces \"NaN\", \"Infinity\", \"-Infinity\"\n}\n```\n\nJava's `StringBuilder.append(double)` delegates to `Double.toString(double)`, which returns the strings `\"NaN\"`, `\"Infinity\"`, and `\"-Infinity\"` for non-finite values. These are **bare literals** in the JSON output — they are not enclosed in quotes and are not valid JSON number syntax per RFC 8259 §6 (which only permits finite numbers). The same issue exists in `formatDoubleArray()` and `formatFloatArray()`, which also call `sb.append(item)` directly for array elements.\n\nThe call chain is:\n```\nMapMessage.getFormattedMessage(new String[]{\"JSON\"})\n  → MapMessage.format(MapFormat.JSON, sb)\n    → MapMessage.asJson(sb)                         [protected]\n      → MapMessageJsonFormatter.format(sb, data)\n        → formatNumber(sb, number) / formatDoubleArray(...) / formatFloatArray(...)\n```\n\n**Fix (PR #4163, merged as commit `19edb23`/squash `c7103d5` on `2.x`; `feadf8eb` on `2.25.x`; `1352b987` on `2.26.x`):**\nThe fix adds two private helper methods that check `Double.isFinite()` / `Float.isFinite()` and, for non-finite values, call `formatString()` to wrap the value in JSON string quotes:\n\n```java\n// FIXED (rel/2.25.5)\nprivate static void formatDouble(StringBuilder sb, double doubleNumber) {\n    if (!Double.isFinite(doubleNumber)) {\n        formatString(sb, Double.toString(doubleNumber));  // quotes: \"NaN\", \"Infinity\", etc.\n    } else {\n        sb.append(doubleNumber);\n    }\n}\n```\n\nAll `sb.append(doubleNumber)` / `sb.append(floatNumber)` / `sb.append(item)` call sites in `formatNumber()`, `formatDoubleArray()`, and `formatFloatArray()` are replaced with calls to `formatDouble()` / `formatFloat()`.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained, idempotent).\n2. **What the script does:**\n   - Installs OpenJDK 17 if not present.\n   - Downloads the official Apache-published `log4j-api` artifacts from Maven Central: `log4j-api-2.25.4.jar` (vulnerable, built from tag `rel/2.25.4`) and `log4j-api-2.25.5.jar` (fixed, built from tag `rel/2.25.5`). Also downloads `gson-2.11.0.jar` for strict JSON validation.\n   - Verifies via `javap` that the vulnerable jar lacks `formatDouble()`/`formatFloat()` helper methods and the fixed jar has them.\n   - Compiles `bundle/repro/NonFiniteJsonTest.java` — a Java harness that creates a concrete `MapMessage` subclass and exercises both `getFormattedMessage(new String[]{\"JSON\"})` (public API) and `asJson(StringBuilder)` (protected, via subclass) with `NaN`, `+Infinity`, `-Infinity` as scalar `double`/`float` and as `double[]`/`float[]` array elements (13 total cases).\n   - Runs the harness against each jar. The harness detects bare (unquoted) non-finite tokens via a character-level scanner and validates each output with a strict RFC 8259 JSON parser (Gson `JsonReader` with `setLenient(false)` + `skipValue()`).\n   - Emits a machine-readable `VERDICT|...` line and writes `bundle/repro/runtime_manifest.json`.\n3. **Expected evidence of reproduction:**\n   - Vulnerable 2.25.4: `bare=13 quoted=0 parseFail=13` — all outputs contain bare `NaN`/`Infinity`/`-Infinity` and fail strict JSON parsing.\n   - Fixed 2.25.5: `bare=0 quoted=13 parseFail=0` — all outputs quote the values as JSON strings and parse successfully.\n\n## Evidence\n\n- **Log files:**\n  - `bundle/logs/vulnerable_output.log` — full harness output for vulnerable log4j-api 2.25.4\n  - `bundle/logs/fixed_output.log` — full harness output for fixed log4j-api 2.25.5\n  - `bundle/logs/compile.log` — javac compilation output\n- **Key excerpts (vulnerable 2.25.4):**\n  ```\n  JSON output: {\"number\":NaN}\n  >> BARE non-finite token detected (invalid JSON per RFC 8259)\n  >> strict JSON parse FAILED (invalid per RFC 8259)\n\n  JSON output: {\"numbers\":[-Infinity]}\n  >> BARE non-finite token detected (invalid JSON per RFC 8259)\n  >> strict JSON parse FAILED (invalid per RFC 8259)\n\n  SUMMARY: bare=13, quoted=0, parseFail=13\n  VERDICT|2.25.4-vulnerable|VULNERABLE|bare=13|quoted=0|parseFail=13\n  ```\n- **Key excerpts (fixed 2.25.5):**\n  ```\n  JSON output: {\"number\":\"NaN\"}\n  >> non-finite value is QUOTED (valid JSON string)\n  >> strict JSON parse OK (valid RFC 8259 JSON)\n\n  JSON output: {\"numbers\":[\"-Infinity\"]}\n  >> non-finite value is QUOTED (valid JSON string)\n  >> strict JSON parse OK (valid RFC 8259 JSON)\n\n  SUMMARY: bare=0, quoted=13, parseFail=0\n  VERDICT|2.25.5-fixed|FIXED|bare=0|quoted=13|parseFail=0\n  ```\n- **javap verification:**\n  - Vulnerable 2.25.4 `MapMessageJsonFormatter` methods: no `formatDouble(`, no `formatFloat(`.\n  - Fixed 2.25.5 `MapMessageJsonFormatter` methods: has `formatDouble(StringBuilder, double)` and `formatFloat(StringBuilder, float)`.\n- **Environment:** OpenJDK 17.0.19, Gson 2.11.0 (strict JSON validator), official Apache log4j-api jars from Maven Central (verified identical to source-built jar from `rel/2.25.4` tag — same file size 351127 bytes).\n- **Runtime manifest:** `bundle/repro/runtime_manifest.json`\n\n## Recommendations / Next Steps\n\n- **Upgrade guidance:** Upgrade `log4j-api` to version 2.25.5+ (2.25.x line) or 2.26.1+ (2.26.x line). If on 3.0.0-alpha, monitor for a fix.\n- **Suggested fix approach:** Already implemented in PR #4163 — gate non-finite `Double`/`Float` values through `formatString()` so they are emitted as JSON strings. This mirrors Jackson's `JsonWriteFeature#WRITE_NAN_AS_STRINGS` behavior.\n- **Testing recommendations:** The fix includes regression tests (`MapMessageTest.testJsonFormatterDoubleNonFiniteSupport` and `testJsonFormatterFloatNonFiniteSupport`) that parameterize over `NaN`, `+Infinity`, `-Infinity` for both `double` and `float`. Any future changes to `MapMessageJsonFormatter` should ensure these tests continue to pass. Consider adding strict-JSON-parser validation to CI for all `MapMessage` JSON output.\n- **Downstream mitigation:** Log-ingestion systems that use lenient JSON parsers (e.g., Gson default mode, some JavaScript engines) may silently accept bare `NaN`/`Infinity`, but strict parsers (RFC 8259) will reject them. Validate log JSON output with a strict parser.\n\n## Additional Notes\n\n- **Idempotency confirmation:** The script was run twice consecutively; both runs produced identical results (exit code 0, `bare=13` for vulnerable, `bare=0` for fixed). The script skips re-downloading jars if they already exist.\n- **Source build verification:** In addition to the Maven Central artifacts, the `log4j-api` module was built from source at tag `rel/2.25.4` using Maven (`mvn -pl log4j-api-java9,log4j-api -am install -DskipTests`). The source-built jar (`log4j-api-2.25.4.jar`, 351127 bytes) is byte-size-identical to the Maven Central artifact, confirming the published jar matches the repository source.\n- **Edge cases covered:** The harness tests both scalar and array forms for both `Double` and `Float`, covering all code paths in `formatNumber()`, `formatDoubleArray()`, and `formatFloatArray()`. The `BigDecimal` path is unaffected (it already uses `toString()` which never produces non-finite tokens).\n- **Scope:** This is a `library_api` claim (`claimed_surface=library_api`, `required_entrypoint_kind=function_call`). The reproduction exercises the real `MapMessage.getFormattedMessage([\"JSON\"])` and `MapMessage.asJson()` functions from the official Apache `log4j-api` artifacts. No service/network boundary is involved.\n","cve_id":"CVE-2026-49844","cwe_id":"CWE-116","source_url":"https://logging.apache.org/security.html#CVE-2026-49844","package":{"name":"org.apache.logging.log4j:log4j-api","ecosystem":"maven","affected_versions":">=2.13.1,<2.25.5; >=2.26.0,<2.26.1; >=3.0.0-alpha1,<=3.0.0-beta2","fixed_version":"2.25.5; 2.26.1","tested_vulnerable":"2.25.4; 2.26.0","tested_patched":"2.25.5; 2.26.1"},"reproduced_at":"2026-07-13T18:47:46.937945+00:00","duration_secs":674.0,"tool_calls":170,"handoffs":2,"total_cost_usd":1.9464422999999995,"agent_costs":{"judge":0.0177789,"repro":0.8454022800000001,"support":0.06597998999999999,"vuln_variant":1.0172811300000002},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.0177789},"repro":{"accounts/fireworks/routers/glm-5p2-fast":0.8454022800000001},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.06597998999999999},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.0172811300000002}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-13T18:47:47.647763+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":10616,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10702,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11445,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":17420,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":8725,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":8743,"category":"other"},{"path":"bundle/vuln_variant/VariantTest.java","filename":"VariantTest.java","size":12996,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":2571,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":2615,"category":"other"},{"path":"bundle/logs/vulnerable_output.log","filename":"vulnerable_output.log","size":3331,"category":"log"},{"path":"bundle/logs/fixed_output.log","filename":"fixed_output.log","size":3118,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":771,"category":"other"},{"path":"bundle/repro/NonFiniteJsonTest.java","filename":"NonFiniteJsonTest.java","size":8809,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":972,"category":"other"},{"path":"bundle/logs/compile.log","filename":"compile.log","size":185,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4601,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3208,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1437,"category":"other"},{"path":"bundle/logs/vuln_variant/variant_2.26.1.log","filename":"variant_2.26.1.log","size":4209,"category":"log"},{"path":"bundle/logs/vuln_variant/variant_2.25.4.log","filename":"variant_2.25.4.log","size":4486,"category":"log"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":8539,"category":"documentation"},{"path":"bundle/logs/vuln_variant/variant_2.25.5.log","filename":"variant_2.25.5.log","size":4209,"category":"log"},{"path":"bundle/logs/vuln_variant/variant_2.26.0.log","filename":"variant_2.26.0.log","size":4486,"category":"log"},{"path":"bundle/logs/vuln_variant/compile.log","filename":"compile.log","size":186,"category":"log"}]}