# Patch Analysis — CVE-2026-49844 (PR #4163)

## Fix Identity

- **PR:** #4163 — "Fix handling of non-finite numbers while encoding `MapMessage` to JSON"
- **Commits:**
  - `2.x` (main): `19edb23e162d6c728a8c2221a240037d389ed300`
  - `2.25.x`: `feadf8eb0b`
  - `2.26.x`: `1352b98712`
- **Files changed:**
  - `log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java` (+30/-6 lines, core fix)
  - `log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java` (+26 lines, regression tests)
  - `src/changelog/.2.x.xml` / release-notes entry (changelog only)

## What the Fix Changes

The fix modifies **three** raw-append sites in `MapMessageJsonFormatter` and adds two private helper methods.

### 1. `formatNumber(StringBuilder, Number)` — scalar `Number` handling

Before (vulnerable, rel/2.25.4):
```java
} else if (number instanceof Double) {
    final double doubleNumber = (Double) number;
    sb.append(doubleNumber);              // <-- bare "NaN"/"Infinity"/"-Infinity"
} else if (number instanceof Float) {
    final float floatNumber = (float) number;
    sb.append(floatNumber);               // <-- bare "NaN"/"Infinity"/"-Infinity"
} ...
} else {                                  // non-Double/Float/BigDecimal/integer Number
    final long longNumber = number.longValue();
    final double doubleValue = number.doubleValue();
    if (Double.compare((double) longNumber, doubleValue) == 0) {
        sb.append(longNumber);
    } else {
        sb.append(doubleValue);           // <-- bare for custom Number w/ non-finite doubleValue()
    }
}
```

After (fixed, rel/2.25.5):
```java
} else if (number instanceof Double) {
    final double doubleNumber = (Double) number;
    formatDouble(sb, doubleNumber);       // gated
} else if (number instanceof Float) {
    final float floatNumber = (float) number;
    formatFloat(sb, floatNumber);         // gated
} ...
} else {
    final long longNumber = number.longValue();
    final double doubleValue = number.doubleValue();
    if (Double.compare((double) longNumber, doubleValue) == 0) {
        sb.append(longNumber);
    } else {
        formatDouble(sb, doubleValue);    // gated (covers custom Number / BigInteger)
    }
}
```
(The `BigDecimal` line changed from `sb.append(decimalNumber.toString())` to `sb.append(decimalNumber)` — behaviorally equivalent; `BigDecimal` is always finite so this is unrelated to the vulnerability.)

### 2 & 3. `formatDoubleArray` / `formatFloatArray` — primitive array elements

Before:
```java
final double item = items[itemIndex];
sb.append(item);                          // <-- bare
...
final float item = items[itemIndex];
sb.append(item);                          // <-- bare
```
After:
```java
final double item = items[itemIndex];
formatDouble(sb, item);                   // gated
...
final float item = items[itemIndex];
formatFloat(sb, item);                    // gated
```

### New helpers
```java
private static void formatDouble(StringBuilder sb, double doubleNumber) {
    // Follows the same logic as Jackson's JsonWriteFeature#WRITE_NAN_AS_STRINGS feature.
    if (!Double.isFinite(doubleNumber)) {
        formatString(sb, Double.toString(doubleNumber));   // quotes: "NaN", "Infinity", "-Infinity"
    } else {
        sb.append(doubleNumber);
    }
}

private static void formatFloat(StringBuilder sb, float floatNumber) {
    // Follows the same logic as Jackson's JsonWriteFeature#WRITE_NAN_AS_STRINGS feature.
    if (!Float.isFinite(floatNumber)) {
        formatString(sb, Float.toString(floatNumber));
    } else {
        sb.append(floatNumber);
    }
}
```
`formatString` wraps the value in `"` quotes and runs `StringBuilders.escapeJson`, producing valid JSON string values.

## What Assumptions the Fix Makes

1. **The sink, not the entry, is gated.** The fix assumes that *every* non-finite `double`/`float` that can appear in the JSON output flows through one of: `formatNumber` (scalar/custom `Number`), `formatDoubleArray`, or `formatFloatArray`. It does **not** try to enumerate or validate at each entry point.
2. **Container branches recurse to the sink.** `formatMap`, `formatList`, `formatCollection`, `formatObjectArray`, and `formatIndexedStringMap` all call `format(sb, value, nextDepth)`, which dispatches `Number` values to `formatNumber`. The fix relies on this recursion to inherit the gating transitively.
3. **`StringBuilderFormattable` is treated as a string.** `formatFormattable` wraps the formattable's `formatTo` output in quotes and JSON-escapes it. The fix assumes this is acceptable (a formattable's output is opaque text, not JSON structure), so it does not gate formattables through `formatDouble`/`formatFloat`.
4. **`BigDecimal`/`BigInteger`/integer `Number` types are finite.** `BigDecimal` is always finite. `BigInteger` is not special-cased in `MapMessageJsonFormatter.formatNumber` (unlike `JsonWriter`, which has an `instanceof BigInteger` branch); it falls into the `else` branch, where a sufficiently large `BigInteger` yields `doubleValue() == Infinity` — but the `else` branch now routes through `formatDouble`, so it is quoted.

## What Code Paths / Inputs the Fix Does NOT Cover

After testing, **none of the materially distinct alternate data paths bypass the fix.** Specifically tested and ruled out:

| Path | Reaches sink via | Covered by fix? |
|------|------------------|-----------------|
| scalar `Double`/`Float` | `formatNumber` `instanceof Double/Float` | ✅ (original repro) |
| `double[]`/`float[]` | `formatDoubleArray`/`formatFloatArray` | ✅ (original repro) |
| nested `java.util.Map` | `formatMap` → `format` → `formatNumber` | ✅ (this variant run) |
| `java.util.List` | `formatList` → `format` → `formatNumber` | ✅ (this variant run) |
| `Object[]` of boxed `Double` | `formatObjectArray` → `format` → `formatNumber` | ✅ (this variant run) |
| non-`List` `Collection` (`Set`/`ArrayDeque`) | `formatCollection` → `format` → `formatNumber` | ✅ (this variant run) |
| custom `Number` subclass (non-finite `doubleValue`) | `formatNumber` `else`-branch | ✅ (this variant run) |
| `BigInteger` overflow → `Infinity` | `formatNumber` `else`-branch | ✅ (this variant run) |
| `StringBuilderFormattable` emitting bare `NaN` | `formatFormattable` (quotes+escapes) | ✅ safe-by-construction (control) |

The branches that carry no floating-point value (`null`, `Boolean`, `char[]`, `boolean[]`, `byte[]`, `short[]`, `int[]`, `long[]`, `String`/`else`) are irrelevant to this vulnerability by type.

## Whether the Fix Is Complete or Leaves Gaps

**The fix is complete within `MapMessageJsonFormatter`.** Empirically, across 18 alternate-path cases per version:
- Vulnerable 2.25.4 / 2.26.0: `bare=17, parseFail=17` (1 quoted = the `StringBuilderFormattable` control).
- Fixed 2.25.5 / 2.26.1: `bare=0, quoted=18, parseFail=0`.

No gap remains in the CVE's sink. The fix's "gate the sink" strategy is robust because the dispatcher's recursion guarantees all `Number`-bearing paths converge on `formatNumber`/`formatDoubleArray`/`formatFloatArray`.

## Behavior Before vs. After the Fix

| Input value (in a `MapMessage`) | Vulnerable output | Fixed output |
|----------------------------------|-------------------|--------------|
| `Double.NaN` (scalar)            | `{"k":NaN}`       | `{"k":"NaN"}` |
| `Double.POSITIVE_INFINITY` (scalar) | `{"k":Infinity}` | `{"k":"Infinity"}` |
| `Double.NEGATIVE_INFINITY` in `List` | `{"k":[Infinity]}` | `{"k":["Infinity"]}` |
| `Double.NaN` in nested `Map`     | `{"k":{"v":NaN}}` | `{"k":{"v":"NaN"}}` |
| custom `Number` w/ `doubleValue()=NaN` | `{"k":NaN}` | `{"k":"NaN"}` |
| `BigInteger(10^400)` (doubleValue=Infinity) | `{"k":Infinity}` | `{"k":"Infinity"}` |
| `StringBuilderFormattable` emitting `NaN` | `{"k":"NaN"}` (already quoted) | `{"k":"NaN"}` (quoted) |

## Target Threat Model Scope

- `SECURITY.adoc` in the repository points to https://logging.apache.org/security.html. Apache Logging acknowledges this class of issue as a vulnerability (CVE-2026-49844 was assigned and fixed in 2.25.5/2.26.1), so non-finite-number JSON encoding is in-scope.
- The CVE is explicitly scoped to `log4j-api`'s `MapMessageJsonFormatter`. The earlier CVE-2026-34481 covered the separate `JsonTemplateLayout`/`JsonWriter` surface, which independently implements the same `isFinite` gating. A defect in a different component (e.g., `log4j-core`'s Jackson-backed `JsonLayout`) would be a separate bug, not a bypass of this fix.
