Browse Source

Make SSRF resolver conditional, sanitize error messages, improve test coverage

Wire SsrfSafeAddressResolverGroup only when SSRF protection is enabled.
Remove "SSRF protection" prefix from error messages to avoid exposing
internal security mechanisms to users. Add 11 new tests covering
isHostnameAllowed, one-arg validateUri, allow-list case-insensitivity,
cloud metadata/loopback overrides, CIDR boundaries, IPv6 unique local,
whitespace parsing, allow-list replacement, and blocked hostname override.
pull/15253/head
Viacheslav Klimov 7 months ago
parent
commit
959a1a84a4
Failed to extract signature
  1. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java
  2. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  3. 6
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java

@ -83,7 +83,7 @@ public final class SsrfSafeAddressResolverGroup extends AddressResolverGroup<Ine
InetSocketAddress resolved = future.getNow();
if (SsrfProtectionValidator.isEnabled() && isBlocked(resolved) && !isOriginalHostAllowed(address)) {
promise.tryFailure(new RuntimeException(
"SSRF protection: resolved address " + resolved.getAddress().getHostAddress() + " is blocked"));
"URI is invalid: host '" + resolved.getAddress().getHostAddress() + "' is not allowed"));
} else {
promise.trySuccess(resolved);
}
@ -114,7 +114,7 @@ public final class SsrfSafeAddressResolverGroup extends AddressResolverGroup<Ine
if (safe.isEmpty()) {
String host = address instanceof InetSocketAddress isa ? isa.getHostString() : address.toString();
promise.tryFailure(new RuntimeException(
"SSRF protection: all resolved addresses for " + host + " are blocked"));
"URI is invalid: host '" + host + "' is not allowed"));
} else {
promise.trySuccess(safe);
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -103,7 +103,6 @@ public class TbHttpClient {
.build();
HttpClient httpClient = HttpClient.create(connectionProvider)
.resolver(SsrfSafeAddressResolverGroup.INSTANCE)
.followRedirect(false)
.runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared))
.doOnConnected(c ->
@ -140,6 +139,10 @@ public class TbHttpClient {
httpClient = httpClient.secure(t -> t.sslContext(sslContext));
}
if (SsrfProtectionValidator.isEnabled()) {
httpClient = httpClient.resolver(SsrfSafeAddressResolverGroup.INSTANCE);
}
validateMaxInMemoryBufferSize(config);
this.webClient = WebClient.builder()

6
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.rest;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.resolver.AddressResolver;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
@ -100,7 +99,7 @@ class SsrfSafeAddressResolverGroupTest {
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("SSRF protection");
.rootCause().hasMessageContaining("is not allowed");
}
@Test
@ -116,7 +115,7 @@ class SsrfSafeAddressResolverGroupTest {
assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.hasRootCauseInstanceOf(RuntimeException.class)
.rootCause().hasMessageContaining("SSRF protection");
.rootCause().hasMessageContaining("is not allowed");
}
@Test
@ -146,4 +145,5 @@ class SsrfSafeAddressResolverGroupTest {
assertThat(results).isNotEmpty();
}
}

Loading…
Cancel
Save