Skip to content

[Security] Unbounded Memory Allocation and Uncontrolled Recursion Leading to DoS #68

@icysun

Description

@icysun

| Discoverer | icysun & Yashon |# Hprose for Java - Unbounded Memory Allocation and Uncontrolled Recursion Leading to Denial of Service
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Report Metadata
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon || Field | Value |
| Discoverer | icysun & Yashon ||-------|-------|
| Discoverer | icysun & Yashon || Product | Hprose for Java |
| Discoverer | icysun & Yashon || Version | 2.0.38 (latest) |
| Discoverer | icysun & Yashon || Commit | a535b86 |
| Discoverer | icysun & Yashon || Repository | https://github.com/hprose/hprose-java |
| Discoverer | icysun & Yashon || Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon || Date | 2026-04-14 |
| Discoverer | icysun & Yashon || Classification | CWE-789, CWE-674 |
| Discoverer | icysun & Yashon || CVSS | 7.5 (High) |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Summary
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |Two denial-of-service vulnerabilities exist in the Hprose deserialization engine (v2.0.38) that allow unauthenticated remote attackers to crash the server with minimal payloads:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |1. Unbounded Memory Allocation (CWE-789): ValueReader.readInt() reads array sizes from network input without any upper bound check. The parsed integer is directly used to allocate arrays (new Object[count], new byte[len], new char[len], etc.). A single 31-byte payload can cause the JVM to attempt allocating ~16GB, triggering OutOfMemoryError.
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |2. Uncontrolled Recursion (CWE-674): ReferenceReader.readArrayList() and other deserialization methods recursively call reader.unserialize() without any depth limit. A deeply nested List structure (~10,000 levels, ~30KB payload) causes StackOverflowError in the handling thread.
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |Both vulnerabilities require no authentication and affect all Hprose deployment modes (HTTP Servlet, TCP Server, WebSocket Server).
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |---
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Vulnerability 1: Unbounded Memory Allocation (CWE-789)
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Root Cause
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |File: src/main/java/hprose/io/unserialize/ValueReader.java
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |readInt() reads decimal digits from the network stream until a terminator byte, with no upper bound check:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// ValueReader.java, readInt() method | **Discoverer** | icysun & Yashon |public final static int readInt(Reader reader, int tag) throws IOException { | **Discoverer** | icysun & Yashon | InputStream stream = reader.stream; | **Discoverer** | icysun & Yashon | int result = 0; | **Discoverer** | icysun & Yashon | int i = stream.read(); | **Discoverer** | icysun & Yashon | if (i == tag) { return result; } | **Discoverer** | icysun & Yashon | boolean neg = false; | **Discoverer** | icysun & Yashon | if (i == '-') { neg = true; i = stream.read(); } | **Discoverer** | icysun & Yashon | while ((i != tag) && (i != -1)) { | **Discoverer** | icysun & Yashon | result = result * 10 + (i - '0'); // No range check | **Discoverer** | icysun & Yashon | i = stream.read(); | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | return (neg ? -result : result); // Can return 0 ~ 2147483647 | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |readCount() and readLength() both call readInt() without additional validation:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// ValueReader.java | **Discoverer** | icysun & Yashon |public final static int readCount(Reader reader) throws IOException { | **Discoverer** | icysun & Yashon | return readInt(reader, TagOpenbrace); // No bounds check | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon | | **Discoverer** | icysun & Yashon |public final static int readLength(Reader reader) throws IOException { | **Discoverer** | icysun & Yashon | return readInt(reader, TagQuote); // No bounds check | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Attack Vector
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |Entry point: HproseService.doInvoke() in src/main/java/hprose/server/HproseService.java
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |When a function name does not match any registered method, remoteMethod == null, and the arguments array is deserialized without type constraints:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// HproseService.java, doInvoke() method | **Discoverer** | icysun & Yashon |int count = reader.readInt(TagOpenbrace); // Reads count from network, no limit | **Discoverer** | icysun & Yashon |// ... | **Discoverer** | icysun & Yashon |if (remoteMethod == null) { | **Discoverer** | icysun & Yashon | args = reader.readArray(count); // Passes unbounded count | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |ReferenceReader.readArray() directly uses the count for memory allocation:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// ReferenceReader.java, readArray() method | **Discoverer** | icysun & Yashon |public final static Object[] readArray(Reader reader, int count) throws IOException { | **Discoverer** | icysun & Yashon | Object[] a = new Object[count]; // ★ Unbounded allocation | **Discoverer** | icysun & Yashon | reader.setRef(a); | **Discoverer** | icysun & Yashon | for (int i = 0; i < count; ++i) { | **Discoverer** | icysun & Yashon | a[i] = reader.unserialize(); | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | reader.skip(TagClosebrace); | **Discoverer** | icysun & Yashon | return a; | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Affected Allocation Points (20+ locations)
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |The unbounded integer propagates to all typed array and collection deserialization methods:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon || Method | Allocation | File |
| Discoverer | icysun & Yashon ||--------|-----------|------|
| Discoverer | icysun & Yashon || readArray(count) | new Object[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readArrayList() | new ArrayList(count) | ReferenceReader.java |
| Discoverer | icysun & Yashon || readHashMap() | new HashMap(count) | ReferenceReader.java |
| Discoverer | icysun & Yashon || readCharArray(count) | new char[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readByteArray(count) | new byte[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readIntArray(count) | new int[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readLongArray(count) | new long[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readStringArray(count) | new String[count] | ReferenceReader.java |
| Discoverer | icysun & Yashon || readChars(len) | new char[len] | ValueReader.java |
| Discoverer | icysun & Yashon || readBytes(len) | new byte[len] | ValueReader.java |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### PoC
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |A 31-byte Hprose RPC call payload that causes new Object[2147483647] (attempting ~16GB allocation):
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon | | **Discoverer** | icysun & Yashon |Cs11"nonexistent"a2147483647{}z | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |Breakdown:
| Discoverer | icysun & Yashon |- C — TagCall
| Discoverer | icysun & Yashon |- s11"nonexistent" — TagString: function name "nonexistent" (11 chars)
| Discoverer | icysun & Yashon |- a2147483647{ — TagList: count = Integer.MAX_VALUE (2,147,483,647)
| Discoverer | icysun & Yashon |- } — TagClosebrace (empty body)
| Discoverer | icysun & Yashon |- z — TagEnd
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Impact
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |- OutOfMemoryError is thrown immediately at new Object[count]
| Discoverer | icysun & Yashon |- OOM is an Error, not an Exception — the afterFilter() catch block only catches IOException, so OOM propagates uncaught
| Discoverer | icysun & Yashon |- On JVMs with limited heap, the entire process crashes
| Discoverer | icysun & Yashon |- On JVMs with large heap, the allocation attempt still causes significant GC pressure and potential pause
| Discoverer | icysun & Yashon |- No authentication required
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### CVSS v3.1: 7.5 (High)
| Discoverer | icysun & Yashon |CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |---
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Vulnerability 2: Uncontrolled Recursion (CWE-674)
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Root Cause
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |File: src/main/java/hprose/io/unserialize/ReferenceReader.java
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |readArrayList() recursively calls reader.unserialize() for each element without any depth tracking:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// ReferenceReader.java, readArrayList() method | **Discoverer** | icysun & Yashon |public final static ArrayList readArrayList(Reader reader) throws IOException { | **Discoverer** | icysun & Yashon | int count = ValueReader.readCount(reader); | **Discoverer** | icysun & Yashon | ArrayList a = new ArrayList(count); | **Discoverer** | icysun & Yashon | reader.setRef(a); | **Discoverer** | icysun & Yashon | if (count > 0) { | **Discoverer** | icysun & Yashon | for (int i = 0; i < count; ++i) { | **Discoverer** | icysun & Yashon | a.add(reader.unserialize()); // ★ Unbounded recursion | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | reader.skip(TagClosebrace); | **Discoverer** | icysun & Yashon | return a; | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |The same issue exists in readHashMap(), readObject(), readMap(), and all collection-type deserialization methods.
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Recursive Call Chain
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon | | **Discoverer** | icysun & Yashon |reader.unserialize() | **Discoverer** | icysun & Yashon | → DefaultUnserializer.unserialize() | **Discoverer** | icysun & Yashon | → case TagList('a'): ReferenceReader.readArrayList(reader) | **Discoverer** | icysun & Yashon | → ValueReader.readCount(reader) | **Discoverer** | icysun & Yashon | → loop: reader.unserialize() | **Discoverer** | icysun & Yashon | → DefaultUnserializer.unserialize() | **Discoverer** | icysun & Yashon | → case TagList('a'): ReferenceReader.readArrayList(reader) | **Discoverer** | icysun & Yashon | → ... (unbounded recursion until StackOverflow) | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### PoC
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |A deeply nested List structure with ~10,000 levels of recursion:
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon | | **Discoverer** | icysun & Yashon |Cs11"nonexistent"a1{a1{a1{...[10000 levels]...a1{i0;}}[10000 closes]...}z | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |Each nesting level adds a1{ (3 bytes), the innermost element is i0; (3 bytes), and each level closes with } (1 byte). Total payload size: ~40KB for 10,000 levels.
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Impact
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |- StackOverflowError is thrown in the thread processing the request
| Discoverer | icysun & Yashon |- StackOverflowError is an Error, not caught by IOException handlers
| Discoverer | icysun & Yashon |- The affected thread is destroyed
| Discoverer | icysun & Yashon |- In thread-pooled environments (Servlet containers), repeated attacks can exhaust the thread pool, causing complete denial of service
| Discoverer | icysun & Yashon |- Each attack costs only ~40KB of bandwidth
| Discoverer | icysun & Yashon |- No authentication required
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### CVSS v3.1: 7.5 (High)
| Discoverer | icysun & Yashon |CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |---
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Attack Prerequisites
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |- None. Both vulnerabilities can be triggered by any remote attacker who can reach the Hprose service endpoint (HTTP port, TCP port, or WebSocket endpoint).
| Discoverer | icysun & Yashon |- Hprose does not provide built-in authentication. Unless the application has added custom filters, all RPC endpoints are publicly accessible.
| Discoverer | icysun & Yashon |- The PoC payload for OOM is only 31 bytes; the PoC payload for StackOverflow is ~40KB. Both are well within default request size limits of any web server or reverse proxy.
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Suggested Fixes
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Fix for Vulnerability 1: Add upper bound checks
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// ValueReader.java | **Discoverer** | icysun & Yashon |private static final int MAX_COUNT = 0x100000; // 1M elements | **Discoverer** | icysun & Yashon | | **Discoverer** | icysun & Yashon |public final static int readCount(Reader reader) throws IOException { | **Discoverer** | icysun & Yashon | int count = readInt(reader, TagOpenbrace); | **Discoverer** | icysun & Yashon | if (count < 0 || count > MAX_COUNT) { | **Discoverer** | icysun & Yashon | throw new IOException("Array count out of range: " + count); | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | return count; | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon | | **Discoverer** | icysun & Yashon |public final static int readLength(Reader reader) throws IOException { | **Discoverer** | icysun & Yashon | int len = readInt(reader, TagQuote); | **Discoverer** | icysun & Yashon | if (len < 0 || len > MAX_COUNT) { | **Discoverer** | icysun & Yashon | throw new IOException("String/bytes length out of range: " + len); | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | return len; | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |### Fix for Vulnerability 2: Add recursion depth limit
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |java | **Discoverer** | icysun & Yashon |// Reader.java | **Discoverer** | icysun & Yashon |private static final int MAX_DEPTH = 256; | **Discoverer** | icysun & Yashon |private int depth = 0; | **Discoverer** | icysun & Yashon | | **Discoverer** | icysun & Yashon |public final Object unserialize() throws IOException { | **Discoverer** | icysun & Yashon | if (++depth > MAX_DEPTH) { | **Discoverer** | icysun & Yashon | throw new IOException("Maximum recursion depth (" + MAX_DEPTH + ") exceeded"); | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon | try { | **Discoverer** | icysun & Yashon | return DefaultUnserializer.instance.read(this); | **Discoverer** | icysun & Yashon | } finally { | **Discoverer** | icysun & Yashon | --depth; | **Discoverer** | icysun & Yashon | } | **Discoverer** | icysun & Yashon |} | **Discoverer** | icysun & Yashon |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## CVE Request
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |We request CVE identifiers for both vulnerabilities:
| Discoverer | icysun & Yashon |- CVE for CWE-789: Unbounded memory allocation in Hprose deserialization engine
| Discoverer | icysun & Yashon |- CVE for CWE-674: Uncontrolled recursion in Hprose deserialization engine
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Disclosure Timeline
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon || Date | Action |
| Discoverer | icysun & Yashon ||------|--------|
| Discoverer | icysun & Yashon || 2026-04-14 | Vulnerabilities discovered |
| Discoverer | icysun & Yashon || 2026-04-14 | Report sent to maintainer |
| Discoverer | icysun & Yashon || 2026-07-13 | Expected 90-day public disclosure (if no fix) |
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |## Attachments
| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |1. poc_hprose_dos_oom.py — PoC for unbounded memory allocation (OOM)
| Discoverer | icysun & Yashon |2. poc_hprose_dos_stackoverflow.py — PoC for uncontrolled recursion (StackOverflow)

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