Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The source of truth for the entries below is [docs/changelog.md](docs/changelog.

## [Unreleased]

### Security

- **Uppercase-scheme bypass in the LLM tool-input guard fixed.** `JsonToolInputGuard` collected candidate URLs with a case-sensitive `http://` / `https://` prefix test, so an uppercase-scheme tool-input URL (`HTTP://169.254.169.254/…`) was never collected and skipped policy validation entirely — bypassing the host allowlist and IP-literal checks. Detection is now case-insensitive. Affects `ssrf-guard-springai` and `ssrf-guard-langchain4j`.

## [3.1.0] — LLM core extraction, LangChain4j, WebClient DNS gap, GraalVM hints

### Added
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and

## [Unreleased]

### Security

- **Uppercase-scheme bypass in the LLM tool-input guard fixed.** `JsonToolInputGuard` collected candidate URLs with a case-sensitive `http://` / `https://` prefix test, so a tool-input URL written with an uppercase scheme (`HTTP://169.254.169.254/…`, `HtTpS://evil.com/`) was never collected and skipped policy validation entirely — bypassing the host allowlist and IP-literal checks. URI schemes are case-insensitive (RFC 3986 §3.1) and the downstream scheme check already used `equalsIgnoreCase`, so this only affected the collection step. Detection is now case-insensitive. Affects `ssrf-guard-springai` and `ssrf-guard-langchain4j`, which both route tool input through this guard.

## [3.1.0] — LLM core extraction, LangChain4j, WebClient DNS gap, GraalVM hints

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ private static void collectUrlLikeStrings(JsonNode node, List<String> out) {

private static boolean looksLikeUrl(String s) {
if (s == null) return false;
String trimmed = s.trim();
// Lower-case before the prefix test: URI schemes are case-insensitive
// (RFC 3986 §3.1), so HTTP:// / HtTpS:// must be detected too. The
// downstream scheme check already uses equalsIgnoreCase; a
// case-sensitive match here would let an uppercase-scheme URL skip
// collection entirely and bypass the policy.
String trimmed = s.trim().toLowerCase();
return trimmed.startsWith("http://") || trimmed.startsWith("https://");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ void throws_when_throw_on_violation_is_true() {
.matches(e -> e.reason() == BlockReason.BLOCKED_IP_LITERAL);
}

@Test
void blocks_uppercase_and_mixed_case_scheme_urls() {
// URI schemes are case-insensitive; an uppercase scheme must not let a
// URL skip collection and bypass the policy.
var guard = new JsonToolInputGuard(policy(List.of("api.example.com")));

String upper = guard.checkOrFormatError("{\"url\":\"HTTP://169.254.169.254/latest/meta-data/\"}");
assertThat(upper)
.isNotNull()
.contains("\"error\":\"ssrf_blocked\"")
.contains("\"reason\":\"blocked_ip_literal\"");

String mixed = guard.checkOrFormatError("{\"request\":{\"target\":\"HtTpS://evil.com/\"}}");
assertThat(mixed).contains("\"error\":\"ssrf_blocked\"");
}

@Test
void still_allows_whitelisted_url_with_uppercase_scheme() {
var guard = new JsonToolInputGuard(policy(List.of("api.example.com")));
assertThat(guard.checkOrFormatError("{\"url\":\"HTTPS://api.example.com/v1\"}")).isNull();
}

@Test
void ignores_non_http_schemes() {
// mailto:, urn:uuid:, file:// — should not trip the URL detector
Expand Down