From 0194549f25dc005a66484cb435ac927d97f8676a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:02:40 +0200 Subject: [PATCH] fix(events): reject malformed pagination cursors as 400 errors Wraps DateTimeParseException from Instant.parse in IllegalArgumentException so the controller maps it to 400. Also rejects cursors with empty instance_id (trailing '|') which would otherwise produce a vacuous keyset predicate. --- .../ClickHouseAgentEventRepository.java | 9 ++++++-- .../ClickHouseAgentEventRepositoryIT.java | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java index 274599aa..020e070f 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java @@ -80,10 +80,15 @@ public class ClickHouseAgentEventRepository implements AgentEventRepository { if (cursor != null && !cursor.isEmpty()) { String decoded = new String(Base64.getUrlDecoder().decode(cursor), StandardCharsets.UTF_8); int bar = decoded.indexOf('|'); - if (bar <= 0) { + if (bar <= 0 || bar == decoded.length() - 1) { throw new IllegalArgumentException("Malformed cursor"); } - Instant cursorTs = Instant.parse(decoded.substring(0, bar)); + Instant cursorTs; + try { + cursorTs = Instant.parse(decoded.substring(0, bar)); + } catch (java.time.format.DateTimeParseException e) { + throw new IllegalArgumentException("Malformed cursor", e); + } String cursorInstance = decoded.substring(bar + 1); sql.append(" AND (timestamp < ? OR (timestamp = ? AND instance_id > ?))"); params.add(Timestamp.from(cursorTs)); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepositoryIT.java index 8bee13c1..d363e949 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepositoryIT.java @@ -227,4 +227,26 @@ class ClickHouseAgentEventRepositoryIT { assertThat(p2.data().get(0).instanceId()).isEqualTo("agent-z"); assertThat(p2.hasMore()).isFalse(); } + + @Test + void queryPage_malformedCursor_invalidTimestamp_throws() { + String raw = "not-a-timestamp|agent-1"; + String cursor = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> repo.queryPage(null, null, null, null, null, cursor, 10)); + } + + @Test + void queryPage_malformedCursor_emptyInstanceId_throws() { + String raw = "2026-04-01T10:00:00Z|"; + String cursor = java.util.Base64.getUrlEncoder().withoutPadding() + .encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> repo.queryPage(null, null, null, null, null, cursor, 10)); + } }