| 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)
| 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, triggeringOutOfMemoryError.| Discoverer | icysun & Yashon |
| Discoverer | icysun & Yashon |2. Uncontrolled Recursion (CWE-674):
ReferenceReader.readArrayList()and other deserialization methods recursively callreader.unserialize()without any depth limit. A deeply nested List structure (~10,000 levels, ~30KB payload) causesStackOverflowErrorin 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()andreadLength()both callreadInt()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()insrc/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 anException— theafterFilter()catch block only catchesIOException, 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 callsreader.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 isi0;(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 |-
StackOverflowErroris thrown in the thread processing the request| Discoverer | icysun & Yashon |- StackOverflowError is an
Error, not caught byIOExceptionhandlers| 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)