Skip to content

Commit 56140b4

Browse files
committed
fix(conf): reject tls_roots with unsafe verification
Reject tls_roots combined with tls_verify=unsafe_off across Sender and QWP config facades. Enforce the invariant in fluent and low-level TLS construction paths, with regression coverage for PEM and password-protected trust stores.
1 parent 28bf04f commit 56140b4

9 files changed

Lines changed: 115 additions & 2 deletions

File tree

core/src/main/java/io/questdb/client/ClientTlsConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public class ClientTlsConfiguration {
3333
private final String trustStorePath;
3434

3535
public ClientTlsConfiguration(String trustStorePath, char[] trustStorePassword, int tlsValidationMode) {
36+
if (trustStorePath != null && tlsValidationMode == TLS_VALIDATION_MODE_NONE) {
37+
throw new IllegalArgumentException("custom trust store cannot be combined with disabled TLS validation");
38+
}
3639
this.trustStorePath = trustStorePath;
3740
this.trustStorePassword = trustStorePassword;
3841
this.tlsValidationMode = tlsValidationMode;

core/src/main/java/io/questdb/client/Sender.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ enum Transport {
934934
*/
935935
final class LineSenderBuilder {
936936
private static final int AUTO_FLUSH_DISABLED = 0;
937+
private static final String TLS_ROOTS_INSECURE_CONFIG_ERROR = "tls_roots cannot be combined with tls_verify=unsafe_off; remove tls_verify to use custom roots, or remove tls_roots to disable certificate validation";
937938
// close() drain timeout. Default applied at build() time. 0 or -1
938939
// means "fast close" (skip the drain entirely); any positive value
939940
// bounds the wait for ackedFsn to catch up to publishedFsn. Uses
@@ -3576,6 +3577,9 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
35763577
if (trustStorePath == null && trustStorePassword != null) {
35773578
throw new LineSenderException("tls_roots_password was configured, but tls_roots is missing");
35783579
}
3580+
if (trustStorePath != null && tlsValidationMode == TlsValidationMode.INSECURE) {
3581+
throw new LineSenderException(TLS_ROOTS_INSECURE_CONFIG_ERROR);
3582+
}
35793583
if (protocol == PROTOCOL_HTTP || protocol == PROTOCOL_WEBSOCKET) {
35803584
if (user != null) {
35813585
httpUsernamePassword(user, password);
@@ -3835,6 +3839,9 @@ static void validateWsConfig(ConfigView view, boolean tls) {
38353839
if (tlsRoots == null && tlsRootsPassword != null) {
38363840
throw new IllegalArgumentException("tls_roots_password requires tls_roots");
38373841
}
3842+
if (tlsRoots != null && "unsafe_off".equals(tlsVerify)) {
3843+
throw new IllegalArgumentException(TLS_ROOTS_INSECURE_CONFIG_ERROR);
3844+
}
38383845
}
38393846

38403847
/**
@@ -3971,6 +3978,9 @@ private void validateParameters() {
39713978
if (!tlsEnabled && tlsValidationMode != TlsValidationMode.DEFAULT) {
39723979
throw new LineSenderException("TLS validation disabled, but TLS was not enabled");
39733980
}
3981+
if (trustStorePath != null && tlsValidationMode == TlsValidationMode.INSECURE) {
3982+
throw new LineSenderException("custom trust store cannot be combined with disabled TLS validation");
3983+
}
39743984
if (keyId != null && bufferCapacity < MIN_BUFFER_SIZE) {
39753985
throw new LineSenderException("Requested buffer too small ")
39763986
.put("[minimalCapacity=").put(MIN_BUFFER_SIZE)
@@ -4172,6 +4182,9 @@ private LineSenderBuilder setCustomTrustStore(String trustStorePath, char[] trus
41724182
if (Chars.isBlank(trustStorePath)) {
41734183
throw new LineSenderException("trust store path cannot be empty nor null");
41744184
}
4185+
if (tlsValidationMode == TlsValidationMode.INSECURE) {
4186+
throw new LineSenderException("custom trust store cannot be configured when TLS validation is disabled");
4187+
}
41754188

41764189
LineSenderBuilder.this.trustStorePath = trustStorePath;
41774190
LineSenderBuilder.this.trustStorePassword = trustStorePassword;
@@ -4190,6 +4203,9 @@ private LineSenderBuilder setCustomTrustStore(String trustStorePath, char[] trus
41904203
* @return an instance of LineSenderBuilder for further configuration
41914204
*/
41924205
public LineSenderBuilder disableCertificateValidation() {
4206+
if (LineSenderBuilder.this.trustStorePath != null) {
4207+
throw new LineSenderException("TLS validation cannot be disabled when a custom trust store is configured");
4208+
}
41934209
LineSenderBuilder.this.tlsValidationMode = TlsValidationMode.INSECURE;
41944210
return LineSenderBuilder.this;
41954211
}

core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpQueryClient.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public class QwpQueryClient implements QuietCloseable {
140140
private static final long DEFAULT_FAILOVER_MAX_BACKOFF_MS = 1_000L;
141141
private static final long DEFAULT_FAILOVER_MAX_DURATION_MS = 30_000L;
142142
private static final int DEFAULT_IO_BUFFER_POOL_SIZE = 4;
143+
private static final String TLS_ROOTS_INSECURE_CONFIG_ERROR = "tls_roots cannot be combined with tls_verify=unsafe_off; remove tls_verify to use custom roots, or remove tls_roots to disable certificate validation";
143144
/**
144145
* How long {@link #connect()} waits to read the {@code SERVER_INFO} frame
145146
* from each endpoint before giving up and moving to the next. 5 seconds is
@@ -550,6 +551,9 @@ public static void validateConfig(ConfigView view, boolean tls) {
550551
if (tlsRoots == null && tlsRootsPassword != null) {
551552
throw new IllegalArgumentException("tls_roots_password requires tls_roots");
552553
}
554+
if (tlsRoots != null && "unsafe_off".equals(tlsVerify)) {
555+
throw new IllegalArgumentException(TLS_ROOTS_INSECURE_CONFIG_ERROR);
556+
}
553557
// Mirror fromConfig's effective values: a missing bound takes its
554558
// default, so the ordering is enforced even when only one key is set
555559
// (e.g. failover_backoff_max_ms alone, below the default initial backoff).

core/src/main/java/io/questdb/client/network/TlsTrustStore.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ public static SSLEngine createSslEngine(
7979
String peerHost,
8080
Class<?> resourceAnchor
8181
) throws CertificateException, IOException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException {
82-
// custom roots with validation disabled would silently skip hostname verification below
83-
assert trustStorePath == null || !insecure;
82+
if (trustStorePath != null && insecure) {
83+
throw new IllegalArgumentException("custom trust store cannot be combined with disabled TLS validation");
84+
}
8485
SSLContext sslContext;
8586
if (trustStorePath != null) {
8687
sslContext = SSLContext.getInstance("TLS");

core/src/test/java/io/questdb/client/test/QuestDBBuilderTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ public void testMalformedIngressConfigRejectedAtBuildWithMinZero() {
114114
"ws::addr=127.0.0.1:1;sf_durability=flush;sender_pool_min=0;query_pool_min=0;", "not yet supported");
115115
}
116116

117+
@Test
118+
public void testTlsRootsWithUnsafeOffRejectedAtBuildWithMinZero() {
119+
assertBuildRejected(
120+
"wss::addr=127.0.0.1:1;tls_roots=/ca.pem;tls_verify=unsafe_off;sender_pool_min=0;query_pool_min=0;",
121+
"tls_roots cannot be combined with tls_verify=unsafe_off"
122+
);
123+
}
124+
117125
@Test
118126
public void testMalformedPoolValueRejectedAtBuild() {
119127
// A non-numeric pool value is rejected at build()'s pool-key resolution,

core/src/test/java/io/questdb/client/test/cutlass/line/LineSenderBuilderTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
public class LineSenderBuilderTest {
4545
private static final String AUTH_TOKEN_KEY1 = "UvuVb1USHGRRT08gEnwN2zGZrvM4MsLQ5brgF6SVkAw=";
4646
private static final String LOCALHOST = "localhost";
47+
private static final String TLS_ROOTS_INSECURE_ERROR = "tls_roots cannot be combined with tls_verify=unsafe_off; remove tls_verify to use custom roots, or remove tls_roots to disable certificate validation";
4748
private static final char[] TRUSTSTORE_PASSWORD = "questdb".toCharArray();
4849
private static final String TRUSTSTORE_PATH = "/keystore/server.keystore";
4950

@@ -187,6 +188,9 @@ public void testConfStringValidation() throws Exception {
187188
assertConfStrError("http::addr=localhost;tls_roots=/some/path;", "custom trust store configured, but TLS was not enabled");
188189
assertConfStrOk("https::addr=localhost;tls_roots=/some/path;protocol_version=2;");
189190
assertConfStrError("http::addr=localhost;tls_roots_password=hunter123;", "tls_roots_password was configured, but tls_roots is missing");
191+
assertConfStrError("tcps::addr=localhost;tls_roots=/ca.pem;tls_verify=unsafe_off;", TLS_ROOTS_INSECURE_ERROR);
192+
assertConfStrError("https::addr=localhost;tls_verify=unsafe_off;tls_roots=/ca.pem;protocol_version=2;", TLS_ROOTS_INSECURE_ERROR);
193+
assertConfStrError("tcps::addr=localhost;tls_roots=/ca.p12;tls_roots_password=secret;tls_verify=unsafe_off;", TLS_ROOTS_INSECURE_ERROR);
190194
assertConfStrError("tcp::addr=localhost;user=foo;", "token cannot be empty nor null");
191195
assertConfStrError("tcp::addr=localhost;username=foo;", "token cannot be empty nor null");
192196
assertConfStrError("tcp::addr=localhost;token=foo;", "TCP token is configured, but user is missing");
@@ -307,6 +311,25 @@ public void testCustomTruststorePasswordCannotBeNull() {
307311
() -> Sender.builder(Sender.Transport.TCP).advancedTls().customTrustStore(TRUSTSTORE_PATH, null));
308312
}
309313

314+
@Test
315+
public void testCustomTruststoreAndDisabledValidationCannotBeCombinedViaRetainedSettings() {
316+
Sender.LineSenderBuilder customRootsBuilder = Sender.builder(Sender.Transport.TCP).enableTls();
317+
Sender.LineSenderBuilder.AdvancedTlsSettings customRootsSettings = customRootsBuilder.advancedTls();
318+
customRootsSettings.customTrustStore(TRUSTSTORE_PATH, TRUSTSTORE_PASSWORD);
319+
assertThrows(
320+
"TLS validation cannot be disabled when a custom trust store is configured",
321+
customRootsSettings::disableCertificateValidation
322+
);
323+
324+
Sender.LineSenderBuilder insecureBuilder = Sender.builder(Sender.Transport.TCP).enableTls();
325+
Sender.LineSenderBuilder.AdvancedTlsSettings insecureSettings = insecureBuilder.advancedTls();
326+
insecureSettings.disableCertificateValidation();
327+
assertThrows(
328+
"custom trust store cannot be configured when TLS validation is disabled",
329+
() -> insecureSettings.customTrustStore(TRUSTSTORE_PATH, TRUSTSTORE_PASSWORD)
330+
);
331+
}
332+
310333
@Test
311334
public void testCustomPemRootsDoNotRequirePassword() throws Exception {
312335
assertMemoryLeak(() -> {

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/LineSenderBuilderWebSocketTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,18 @@ public void testWssConfigString_tlsRootsPasswordWithoutRoots_fails() {
10371037
);
10381038
}
10391039

1040+
@Test
1041+
public void testWssConfigString_tlsRootsWithUnsafeOff_fails() {
1042+
assertBadConfig(
1043+
"wss::addr=localhost:9000;tls_roots=/ca.pem;tls_verify=unsafe_off;",
1044+
"tls_roots cannot be combined with tls_verify=unsafe_off"
1045+
);
1046+
assertBadConfig(
1047+
"wss::addr=localhost:9000;tls_verify=unsafe_off;tls_roots=/ca.p12;tls_roots_password=secret;",
1048+
"tls_roots cannot be combined with tls_verify=unsafe_off"
1049+
);
1050+
}
1051+
10401052
@Test
10411053
public void testWsConfigString_withToken() throws Exception {
10421054
assertMemoryLeak(() -> {

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/QwpQueryClientFromConfigTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
*/
4141
public class QwpQueryClientFromConfigTest {
4242

43+
private static final String TLS_ROOTS_INSECURE_ERROR = "tls_roots cannot be combined with tls_verify=unsafe_off; remove tls_verify to use custom roots, or remove tls_roots to disable certificate validation";
44+
4345
@Test
4446
public void testAddrAcceptsHostWithoutPort() {
4547
// Host-only accepted; port defaults to the public DEFAULT_WS_PORT constant.
@@ -983,6 +985,18 @@ public void testTlsRootsWithPasswordAccepted() {
983985
assertParses("wss::addr=db:9000;tls_roots=/etc/qdb/ca.p12;tls_roots_password=secret;");
984986
}
985987

988+
@Test
989+
public void testTlsRootsWithUnsafeOffRejected() {
990+
assertReject(
991+
"wss::addr=db:9000;tls_roots=/etc/qdb/ca.pem;tls_verify=unsafe_off;",
992+
TLS_ROOTS_INSECURE_ERROR
993+
);
994+
assertReject(
995+
"wss::addr=db:9000;tls_verify=unsafe_off;tls_roots=/etc/qdb/ca.p12;tls_roots_password=secret;",
996+
TLS_ROOTS_INSECURE_ERROR
997+
);
998+
}
999+
9861000
@Test
9871001
public void testPemTlsRootsWithoutPasswordAccepted() {
9881002
assertParses("wss::addr=db:9000;tls_roots=/etc/qdb/ca.pem;");

core/src/test/java/io/questdb/client/test/network/TlsTrustStoreTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package io.questdb.client.test.network;
2626

27+
import io.questdb.client.ClientTlsConfiguration;
2728
import io.questdb.client.network.TlsTrustStore;
2829
import org.junit.Assert;
2930
import org.junit.Test;
@@ -43,6 +44,37 @@
4344
public class TlsTrustStoreTest {
4445

4546
private static final String RESOURCE_PREFIX = "/io/questdb/client/test/network/";
47+
private static final String TLS_ROOTS_INSECURE_ERROR = "custom trust store cannot be combined with disabled TLS validation";
48+
49+
@Test
50+
public void testClientTlsConfigurationRejectsCustomRootsWithDisabledValidation() {
51+
try {
52+
new ClientTlsConfiguration(
53+
"/ca.pem",
54+
null,
55+
ClientTlsConfiguration.TLS_VALIDATION_MODE_NONE
56+
);
57+
Assert.fail("expected custom roots with disabled validation to be rejected");
58+
} catch (IllegalArgumentException e) {
59+
Assert.assertEquals(TLS_ROOTS_INSECURE_ERROR, e.getMessage());
60+
}
61+
}
62+
63+
@Test
64+
public void testCreateSslEngineRejectsCustomRootsWithDisabledValidation() throws Exception {
65+
try {
66+
TlsTrustStore.createSslEngine(
67+
"/ca.pem",
68+
null,
69+
true,
70+
"localhost",
71+
TlsTrustStoreTest.class
72+
);
73+
Assert.fail("expected custom roots with disabled validation to be rejected");
74+
} catch (IllegalArgumentException e) {
75+
Assert.assertEquals(TLS_ROOTS_INSECURE_ERROR, e.getMessage());
76+
}
77+
}
4678

4779
@Test
4880
public void testJksTrustStoreStillSupported() throws Exception {

0 commit comments

Comments
 (0)