Skip to content

Commit 2b58183

Browse files
committed
fix: prevent CPU runaway in HttpNodesStatsAction JSON parsing (#18)
Three parse methods introduced in #14 were missing `parser.nextToken()` before calling `consumeObject()` or sub-parse methods when the current token was START_OBJECT. This caused `consumeObject()` to consume tokens beyond its scope (including sibling fields), corrupting the parser state and eventually leading to an infinite loop at EOF — spinning all eshttp threads at 100% CPU each. Fixes: - Add `parser.nextToken()` in parseSearchBackpressureStats, parseTaskCancellationStats, and parseSearchPipelineStats - Add null-token (EOF) guards in fromXContent, parseNodes, parseNodeStats, and consumeObject to throw IOException instead of spinning - Add START_ARRAY handling in consumeObject - Remove orphan `new ArrayList<>()` in parseNodeStats Tests: add 61 unit tests covering token boundary verification, field ordering, multiple nodes, partial sub-fields, deeply nested structures, truncated JSON, and concurrent parsing.
1 parent 005ad1c commit 2b58183

2 files changed

Lines changed: 1051 additions & 1 deletion

File tree

src/main/java/org/codelibs/fesen/client/action/HttpNodesStatsAction.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ protected NodesStatsResponse fromXContent(final XContentParser parser) throws IO
159159
ClusterName clusterName = ClusterName.DEFAULT;
160160
XContentParser.Token token;
161161
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
162+
if (token == null) {
163+
throw new IOException("Unexpected end of JSON stream while parsing NodesStatsResponse");
164+
}
162165
if (token == XContentParser.Token.FIELD_NAME) {
163166
fieldName = parser.currentName();
164167
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -181,6 +184,9 @@ protected List<NodeStats> parseNodes(final XContentParser parser) throws IOExcep
181184
String fieldName = null;
182185
XContentParser.Token token;
183186
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
187+
if (token == null) {
188+
throw new IOException("Unexpected end of JSON stream while parsing nodes");
189+
}
184190
if (token == XContentParser.Token.FIELD_NAME) {
185191
fieldName = parser.currentName();
186192
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -193,7 +199,6 @@ protected List<NodeStats> parseNodes(final XContentParser parser) throws IOExcep
193199
}
194200

195201
protected NodeStats parseNodeStats(final XContentParser parser, final String nodeId) throws IOException {
196-
new ArrayList<>();
197202
String fieldName = null;
198203
String nodeName = "";
199204
long timestamp = 0;
@@ -230,6 +235,9 @@ protected NodeStats parseNodeStats(final XContentParser parser, final String nod
230235
XContentParser.Token token;
231236
TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 0);
232237
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
238+
if (token == null) {
239+
throw new IOException("Unexpected end of JSON stream while parsing node stats for " + nodeId);
240+
}
233241
if (token == XContentParser.Token.FIELD_NAME) {
234242
fieldName = parser.currentName();
235243
} else if (token == XContentParser.Token.START_OBJECT) {
@@ -399,6 +407,7 @@ protected SearchBackpressureStats parseSearchBackpressureStats(final XContentPar
399407
if (token == XContentParser.Token.FIELD_NAME) {
400408
fieldName = parser.currentName();
401409
} else if (token == XContentParser.Token.START_OBJECT) {
410+
parser.nextToken();
402411
if ("search_task".equals(fieldName) || "search_shard_task".equals(fieldName)) {
403412
consumeObject(parser);
404413
} else {
@@ -544,6 +553,7 @@ protected TaskCancellationStats parseTaskCancellationStats(final XContentParser
544553
if (token == XContentParser.Token.FIELD_NAME) {
545554
fieldName = parser.currentName();
546555
} else if (token == XContentParser.Token.START_OBJECT) {
556+
parser.nextToken();
547557
if ("search_task".equals(fieldName)) {
548558
searchTaskCancellationStats = parseSearchTaskCancellationStats(parser);
549559
} else if ("search_shard_task".equals(fieldName)) {
@@ -606,6 +616,7 @@ protected SearchPipelineStats parseSearchPipelineStats(final XContentParser pars
606616
if (token == XContentParser.Token.FIELD_NAME) {
607617
fieldName = parser.currentName();
608618
} else if (token == XContentParser.Token.START_OBJECT) {
619+
parser.nextToken();
609620
if ("total_request".equals(fieldName)) {
610621
totalRequestStats = parseOperationStats(parser);
611622
} else if ("total_response".equals(fieldName)) {
@@ -2322,9 +2333,14 @@ protected int[] parseNodeResults(final XContentParser parser) throws IOException
23222333
protected void consumeObject(final XContentParser parser) throws IOException {
23232334
XContentParser.Token token;
23242335
while ((token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
2336+
if (token == null) {
2337+
throw new IOException("Unexpected end of JSON stream while consuming object");
2338+
}
23252339
if (token == XContentParser.Token.START_OBJECT) {
23262340
parser.nextToken();
23272341
consumeObject(parser);
2342+
} else if (token == XContentParser.Token.START_ARRAY) {
2343+
parser.skipChildren();
23282344
}
23292345
parser.nextToken();
23302346
}

0 commit comments

Comments
 (0)