From 99b702b134b7a275c5ac48f3d8d1005b30e40d6b Mon Sep 17 00:00:00 2001
From: BenCodez <17074231+BenCodez@users.noreply.github.com>
Date: Fri, 2 Oct 2026 17:23:51 -0600
Subject: [PATCH 1/4] Use MySQL JDBC scheme for MariaDB driver fallback
---
.../sql/mysql/ConnectionManager.java | 3 +-
.../sql/mysql/MariaDbDriverFallbackTest.java | 93 +++++++++++++++++++
docs/shared-libraries.md | 8 ++
3 files changed, 103 insertions(+), 1 deletion(-)
create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
index d163905..a188efb 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
@@ -261,7 +261,8 @@ String buildJdbcUrl(String driverClassName) {
if (postgreSqlTlsMode != PostgreSqlTlsMode.LEGACY) {
throw new IllegalArgumentException("PostgreSqlTlsMode applies only to PostgreSQL connections");
}
- boolean maria = (dbType == DbType.MARIADB) || "org.mariadb.jdbc.Driver".equals(driverClassName);
+ boolean maria = !"com.mysql.cj.jdbc.Driver".equals(driverClassName)
+ && (dbType == DbType.MARIADB || "org.mariadb.jdbc.Driver".equals(driverClassName));
String base = maria ? String.format("jdbc:mariadb://%s:%s/%s", host, port, database)
: String.format("jdbc:mysql://%s:%s/%s", host, port, database);
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
new file mode 100644
index 0000000..17692b9
--- /dev/null
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
@@ -0,0 +1,93 @@
+package com.bencodez.simpleapi.sql.mysql;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.tools.ToolProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import com.bencodez.simpleapi.tests.shared.SharedRuntimeClasspath;
+
+class MariaDbDriverFallbackTest {
+ @TempDir Path directory;
+
+ @Test void mysqlFallbackUsesMysqlSchemeAndKeepsConfiguredOptions() {
+ ConnectionManager manager = manager(DbType.MARIADB);
+ manager.setUseSSL(true);
+ manager.setPublicKeyRetrieval(true);
+ manager.setStr("&customOption=retained");
+ String url = manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver");
+ assertTrue(url.startsWith("jdbc:mysql://localhost:3306/votes?"));
+ assertTrue(url.contains("useSSL=true"));
+ assertTrue(url.contains("allowPublicKeyRetrieval=true"));
+ assertTrue(url.endsWith("&customOption=retained"));
+ assertEquals(DbType.MARIADB, manager.getDbType());
+ }
+
+ @Test void nativeMariaDbDriverRetainsMariaDbScheme() {
+ assertTrue(manager(DbType.MARIADB).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
+ }
+
+ @Test void explicitMariaDbDriverOverridesMysqlTypeAsBefore() {
+ assertTrue(manager(DbType.MYSQL).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:"));
+ }
+
+ @Test void customMariaDbDriverRetainsConfiguredScheme() {
+ assertTrue(manager(DbType.MARIADB).buildJdbcUrl("example.CustomDriver").startsWith("jdbc:mariadb:"));
+ }
+
+ @Test void mysqlAndPostgresqlSelectionsRemainUnchanged() {
+ assertTrue(manager(DbType.MYSQL).buildJdbcUrl("com.mysql.cj.jdbc.Driver").startsWith("jdbc:mysql:"));
+ assertTrue(manager(DbType.POSTGRESQL).buildJdbcUrl("org.postgresql.Driver").startsWith("jdbc:postgresql:"));
+ }
+
+ @Test void absentMariaDbDriverResolvesToDriverThatAcceptsGeneratedUrl() throws Exception {
+ try (var loader = isolatedMysqlDriver()) {
+ assertThrows(ClassNotFoundException.class, () -> loader.loadClass("org.mariadb.jdbc.Driver"));
+ Class> type = loader.loadClass(ConnectionManager.class.getName());
+ Object manager = type.getConstructor(String.class, String.class, String.class, String.class, String.class)
+ .newInstance("localhost", "3306", "test", "", "votes");
+ Class> dbType = loader.loadClass(DbType.class.getName());
+ type.getMethod("setDbType", dbType).invoke(manager, dbType.getField("MARIADB").get(null));
+ var resolve = type.getDeclaredMethod("resolveDriver"); resolve.setAccessible(true);
+ assertEquals("com.mysql.cj.jdbc.Driver", resolve.invoke(manager));
+ var build = type.getDeclaredMethod("buildJdbcUrl", String.class); build.setAccessible(true);
+ String url = (String) build.invoke(manager, resolve.invoke(manager));
+ java.sql.Driver driver = (java.sql.Driver) loader.loadClass("com.mysql.cj.jdbc.Driver")
+ .getConstructor().newInstance();
+ assertTrue(driver.acceptsURL(url));
+ type.getMethod("setMariadbFallbackToMysqlDriver", boolean.class).invoke(manager, false);
+ InvocationTargetException rejected = assertThrows(InvocationTargetException.class, () -> resolve.invoke(manager));
+ assertInstanceOf(ClassNotFoundException.class, rejected.getCause());
+ }
+ }
+
+ private java.net.URLClassLoader isolatedMysqlDriver() throws Exception {
+ Path source = directory.resolve("com/mysql/cj/jdbc/Driver.java");
+ Files.createDirectories(source.getParent());
+ Files.writeString(source, """
+ package com.mysql.cj.jdbc;
+ public class Driver implements java.sql.Driver {
+ public boolean acceptsURL(String url) { return url.startsWith("jdbc:mysql:"); }
+ public java.sql.Connection connect(String url, java.util.Properties p) { return null; }
+ public java.sql.DriverPropertyInfo[] getPropertyInfo(String u, java.util.Properties p) { return new java.sql.DriverPropertyInfo[0]; }
+ public int getMajorVersion() { return 1; }
+ public int getMinorVersion() { return 0; }
+ public boolean jdbcCompliant() { return false; }
+ public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
+ }
+ """);
+ assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null,
+ "-proc:none", "-d", directory.toString(), source.toString()));
+ return SharedRuntimeClasspath.open(ConnectionManager.class.getProtectionDomain().getCodeSource().getLocation(),
+ directory.toUri().toURL());
+ }
+
+ private ConnectionManager manager(DbType type) {
+ ConnectionManager result = new ConnectionManager("localhost", "3306", "test", "", "votes");
+ result.setDbType(type);
+ return result;
+ }
+}
diff --git a/docs/shared-libraries.md b/docs/shared-libraries.md
index b58f540..0ba2ae5 100644
--- a/docs/shared-libraries.md
+++ b/docs/shared-libraries.md
@@ -202,3 +202,11 @@ is different: it reads a replacement before publishing it and throws
last valid active configuration. The failed reload does not rewrite the file.
Callers must abort runtime reconfiguration when reload fails. An intentionally
empty, valid YAML document remains accepted.
+
+### MariaDB JDBC fallback
+
+When the configured MariaDB driver is absent and the existing MySQL-driver
+fallback is enabled, the resolved MySQL Connector/J driver uses a `jdbc:mysql:`
+URL. The configured database type, TLS/options, host, and database remain intact.
+Native MariaDB and custom MariaDB driver overrides retain the MariaDB scheme;
+the explicit fallback opt-out remains supported.
From 96d540adc53425a4c1c856c7f3d1895b16de266d Mon Sep 17 00:00:00 2001
From: BenCodez <17074231+BenCodez@users.noreply.github.com>
Date: Fri, 2 Oct 2026 18:19:33 -0600
Subject: [PATCH 2/4] Preserve mandatory TLS during MariaDB Connector/J
fallback
---
SimpleAPI/pom.xml | 6 +
.../sql/mysql/ConnectionManager.java | 33 ++++-
.../sql/mysql/MariaDbDriverFallbackTest.java | 3 +-
.../sql/mysql/MariaDbFallbackTlsTest.java | 122 ++++++++++++++++++
docs/shared-libraries.md | 8 ++
5 files changed, 170 insertions(+), 2 deletions(-)
create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
diff --git a/SimpleAPI/pom.xml b/SimpleAPI/pom.xml
index 7bd56c1..56f4429 100644
--- a/SimpleAPI/pom.xml
+++ b/SimpleAPI/pom.xml
@@ -298,6 +298,12 @@
6.1.2
test
+
+ com.mysql
+ mysql-connector-j
+ 9.5.0
+ test
+
org.mockito
mockito-core
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
index a188efb..a0a42d0 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
@@ -266,12 +266,43 @@ String buildJdbcUrl(String driverClassName) {
String base = maria ? String.format("jdbc:mariadb://%s:%s/%s", host, port, database)
: String.format("jdbc:mysql://%s:%s/%s", host, port, database);
+ String fallbackTls = !maria && dbType == DbType.MARIADB && useSSL
+ ? requiredMysqlFallbackTls(extra) : "";
return base + "?useSSL=" + useSSL + "&allowMultiQueries=true" + "&rewriteBatchedStatements=true"
+ "&useDynamicCharsetInfo=false" + "&allowPublicKeyRetrieval=" + publicKeyRetrieval
+ "&tcpKeepAlive=true" + "&connectTimeout=10000" + "&socketTimeout=30000" + "&serverTimezone=UTC"
- + extra;
+ + extra + fallbackTls;
}
+ /** Connector/J useSSL=true alone permits plaintext fallback; MariaDB's flag does not. */
+ private String requiredMysqlFallbackTls(String extra) {
+ String mode = null;
+ boolean verifyCertificate = false;
+ String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
+ for (String parameter : options.split("&", -1)) {
+ String[] pair = parameter.split("=", 2);
+ String key;
+ String value;
+ try {
+ key = URLDecoder.decode(pair[0], StandardCharsets.UTF_8).trim().toLowerCase(Locale.ROOT);
+ value = pair.length == 2 ? URLDecoder.decode(pair[1], StandardCharsets.UTF_8).trim() : "";
+ } catch (IllegalArgumentException invalid) {
+ throw new IllegalArgumentException("Invalid encoded parameter in MariaDB fallback Line");
+ }
+ if (key.equals("sslmode")) {
+ String selected = value.toUpperCase(Locale.ROOT);
+ if (mode != null || !(selected.equals("REQUIRED") || selected.equals("VERIFY_CA")
+ || selected.equals("VERIFY_IDENTITY"))) {
+ throw new IllegalArgumentException("MariaDB UseSSL requires a single mandatory Connector/J TLS mode");
+ }
+ mode = selected;
+ } else if (key.equals("verifyservercertificate") && value.equalsIgnoreCase("true")) {
+ verifyCertificate = true;
+ }
+ }
+ return "&sslMode=" + (mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED");
+ }
+
private void rejectConflictingPostgreSqlTlsOptions(String extra) {
String options = extra.startsWith("?") || extra.startsWith("&") ? extra.substring(1) : extra;
for (String parameter : options.split("&", -1)) {
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
index 17692b9..c702894 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
@@ -22,7 +22,8 @@ class MariaDbDriverFallbackTest {
assertTrue(url.startsWith("jdbc:mysql://localhost:3306/votes?"));
assertTrue(url.contains("useSSL=true"));
assertTrue(url.contains("allowPublicKeyRetrieval=true"));
- assertTrue(url.endsWith("&customOption=retained"));
+ assertTrue(url.contains("&customOption=retained"));
+ assertTrue(url.endsWith("&sslMode=REQUIRED"));
assertEquals(DbType.MARIADB, manager.getDbType());
}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
new file mode 100644
index 0000000..b2fbce0
--- /dev/null
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
@@ -0,0 +1,122 @@
+package com.bencodez.simpleapi.sql.mysql;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayOutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class MariaDbFallbackTlsTest {
+ @Test void sslEnabledFallbackRequiresTls() {
+ assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED"));
+ }
+
+ @Test void explicitStrongerModesRemainIntact() {
+ for (String mode : new String[]{"REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"}) {
+ ConnectionManager manager = fallback();
+ manager.setStr("&sslMode=" + mode);
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode));
+ }
+ }
+
+ @Test void weakMalformedAndDuplicateModesFailClosed() {
+ for (String options : new String[]{"&sslMode=DISABLED", "&sslMode=PREFERRED", "&sslMode=",
+ "&sslMode=unknown", "&ssl%4dode=PREFERRED", "&sslMode=%50REFERRED",
+ "&sslMode=REQUIRED&sslMode=DISABLED", "&sslMode=VERIFY_CA&sslMode=REQUIRED",
+ "&sslMode=%GG"}) {
+ ConnectionManager manager = fallback();
+ manager.setStr(options);
+ assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
+ }
+ }
+
+ @Test void legacyCertificateVerificationRemainsEnabled() {
+ ConnectionManager manager = fallback();
+ manager.setStr("&verifyServerCertificate=true&customOption=retained");
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA"));
+ }
+
+ @Test void ordinaryMysqlNativeMariaAndSslDisabledRemainUnchanged() {
+ ConnectionManager manager = fallback();
+ assertFalse(manager.buildJdbcUrl("org.mariadb.jdbc.Driver").contains("sslMode="));
+ manager.setDbType(DbType.MYSQL);
+ assertFalse(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").contains("sslMode="));
+ manager.setDbType(DbType.MARIADB);
+ manager.setUseSSL(false);
+ manager.setStr("&sslMode=DISABLED");
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=DISABLED"));
+ }
+
+ @Test void actualConnectorRejectsNonTlsGreetingBeforeSendingCredentials() throws Exception {
+ try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
+ server.setSoTimeout(5000);
+ var worker = Executors.newSingleThreadExecutor();
+ try {
+ var peer = worker.submit(() -> {
+ try (var socket = server.accept()) {
+ socket.setSoTimeout(5000);
+ socket.getOutputStream().write(nonTlsGreeting());
+ socket.getOutputStream().flush();
+ return socket.getInputStream().read();
+ }
+ });
+ ConnectionManager manager = fallback();
+ manager.setHost(server.getInetAddress().getHostAddress());
+ manager.setPort(Integer.toString(server.getLocalPort()));
+ manager.setStr("&connectTimeout=2000&socketTimeout=2000");
+ Properties properties = new Properties();
+ properties.setProperty("user", "test-user");
+ properties.setProperty("password", "test-only-not-a-secret");
+ SQLException failure = assertThrows(SQLException.class, () ->
+ new com.mysql.cj.jdbc.Driver().connect(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"), properties));
+ assertTrue(failure.getMessage().contains("SSL"), failure::getMessage);
+ assertEquals(-1, peer.get(5, TimeUnit.SECONDS), "No authentication packet may be sent to a non-TLS peer");
+ } finally {
+ worker.shutdownNow();
+ }
+ }
+ }
+
+ private static ConnectionManager fallback() {
+ ConnectionManager manager = new ConnectionManager("localhost", "3306", "test", "", "votes");
+ manager.setDbType(DbType.MARIADB);
+ manager.setUseSSL(true);
+ return manager;
+ }
+
+ // Protocol-10 greeting with protocol-41, secure connection and plugin authentication,
+ // deliberately omitting CLIENT_SSL. No actual database or external network is needed.
+ private static byte[] nonTlsGreeting() {
+ ByteArrayOutputStream payload = new ByteArrayOutputStream();
+ payload.write(10);
+ payload.writeBytes("8.0.0-test\0".getBytes(StandardCharsets.US_ASCII));
+ payload.writeBytes(new byte[]{1, 0, 0, 0});
+ payload.writeBytes("abcdefgh".getBytes(StandardCharsets.US_ASCII));
+ payload.write(0);
+ int capabilities = 1 | 512 | 8192 | 32768 | 0x80000;
+ payload.write(capabilities & 255);
+ payload.write((capabilities >>> 8) & 255);
+ payload.write(45);
+ payload.write(2);
+ payload.write(0);
+ payload.write((capabilities >>> 16) & 255);
+ payload.write((capabilities >>> 24) & 255);
+ payload.write(21);
+ payload.writeBytes(new byte[10]);
+ payload.writeBytes("ijklmnopqrst\0".getBytes(StandardCharsets.US_ASCII));
+ payload.writeBytes("mysql_native_password\0".getBytes(StandardCharsets.US_ASCII));
+ ByteArrayOutputStream packet = new ByteArrayOutputStream();
+ packet.write(payload.size() & 255);
+ packet.write((payload.size() >>> 8) & 255);
+ packet.write((payload.size() >>> 16) & 255);
+ packet.write(0);
+ packet.writeBytes(payload.toByteArray());
+ return packet.toByteArray();
+ }
+}
diff --git a/docs/shared-libraries.md b/docs/shared-libraries.md
index 0ba2ae5..c4abb93 100644
--- a/docs/shared-libraries.md
+++ b/docs/shared-libraries.md
@@ -210,3 +210,11 @@ fallback is enabled, the resolved MySQL Connector/J driver uses a `jdbc:mysql:`
URL. The configured database type, TLS/options, host, and database remain intact.
Native MariaDB and custom MariaDB driver overrides retain the MariaDB scheme;
the explicit fallback opt-out remains supported.
+
+For MariaDB configured with `UseSSL=true`, Connector/J fallback requires TLS
+(`sslMode=REQUIRED`) instead of permitting a plaintext downgrade. Stronger
+`VERIFY_CA`/`VERIFY_IDENTITY` modes in `Line` remain supported; weaker or ambiguous
+`sslMode` overrides are rejected. Legacy certificate verification requests remain
+verified. This requires encryption but does not claim server identity verification
+without the appropriate verified mode. Native MariaDB, ordinary MySQL settings,
+and explicitly disabled TLS remain unchanged.
From 52ef718adc7349863ef3d674dce7496f1aa0c459 Mon Sep 17 00:00:00 2001
From: BenCodez <17074231+BenCodez@users.noreply.github.com>
Date: Sat, 3 Oct 2026 10:01:43 -0600
Subject: [PATCH 3/4] Require MariaDB fallback TLS with legacy Connector/J
---
.github/workflows/maven.yml | 3 +++
SimpleAPI/pom.xml | 17 ++++++++++++++---
.../simpleapi/sql/mysql/ConnectionManager.java | 8 ++++++--
.../sql/mysql/MariaDbDriverFallbackTest.java | 2 +-
.../sql/mysql/MariaDbFallbackTlsTest.java | 9 +++++----
docs/shared-libraries.md | 4 ++--
6 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 206bbaa..3d79161 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -27,3 +27,6 @@ jobs:
- name: Build with Maven
run: mvn -B -f SimpleAPI/pom.xml package
+
+ - name: Verify legacy Connector/J fallback TLS
+ run: mvn -B -f SimpleAPI/pom.xml -Plegacy-mysql-tls-test -Dtest=MariaDbFallbackTlsTest test
diff --git a/SimpleAPI/pom.xml b/SimpleAPI/pom.xml
index 56f4429..153fdb1 100644
--- a/SimpleAPI/pom.xml
+++ b/SimpleAPI/pom.xml
@@ -13,6 +13,9 @@
${maven.build.timestamp}
yyyy-MM-dd HH:mm
NOTSET
+ com.mysql
+ mysql-connector-j
+ 9.5.0
21
21
21
@@ -299,9 +302,9 @@
test
- com.mysql
- mysql-connector-j
- 9.5.0
+ ${mysql.connector.test.group}
+ ${mysql.connector.test.artifact}
+ ${mysql.connector.test.version}
test
@@ -360,6 +363,14 @@
+
+ legacy-mysql-tls-test
+
+ mysql
+ mysql-connector-java
+ 8.0.12
+
+
normal
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
index a0a42d0..ea7357e 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
@@ -274,7 +274,7 @@ String buildJdbcUrl(String driverClassName) {
+ extra + fallbackTls;
}
- /** Connector/J useSSL=true alone permits plaintext fallback; MariaDB's flag does not. */
+ /** Require TLS on both modern and pre-8.0.13 Connector/J when MariaDB SSL is enabled. */
private String requiredMysqlFallbackTls(String extra) {
String mode = null;
boolean verifyCertificate = false;
@@ -296,11 +296,15 @@ private String requiredMysqlFallbackTls(String extra) {
throw new IllegalArgumentException("MariaDB UseSSL requires a single mandatory Connector/J TLS mode");
}
mode = selected;
+ } else if (key.equals("usessl") || key.equals("requiressl")) {
+ if (!value.equalsIgnoreCase("true")) {
+ throw new IllegalArgumentException("MariaDB UseSSL forbids disabling Connector/J TLS");
+ }
} else if (key.equals("verifyservercertificate") && value.equalsIgnoreCase("true")) {
verifyCertificate = true;
}
}
- return "&sslMode=" + (mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED");
+ return "&sslMode=" + (mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED") + "&requireSSL=true";
}
private void rejectConflictingPostgreSqlTlsOptions(String extra) {
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
index c702894..9cc3741 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java
@@ -23,7 +23,7 @@ class MariaDbDriverFallbackTest {
assertTrue(url.contains("useSSL=true"));
assertTrue(url.contains("allowPublicKeyRetrieval=true"));
assertTrue(url.contains("&customOption=retained"));
- assertTrue(url.endsWith("&sslMode=REQUIRED"));
+ assertTrue(url.endsWith("&sslMode=REQUIRED&requireSSL=true"));
assertEquals(DbType.MARIADB, manager.getDbType());
}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
index b2fbce0..7c0a914 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
@@ -14,14 +14,14 @@
class MariaDbFallbackTlsTest {
@Test void sslEnabledFallbackRequiresTls() {
- assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED"));
+ assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED&requireSSL=true"));
}
@Test void explicitStrongerModesRemainIntact() {
for (String mode : new String[]{"REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"}) {
ConnectionManager manager = fallback();
manager.setStr("&sslMode=" + mode);
- assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode));
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true"));
}
}
@@ -29,7 +29,8 @@ class MariaDbFallbackTlsTest {
for (String options : new String[]{"&sslMode=DISABLED", "&sslMode=PREFERRED", "&sslMode=",
"&sslMode=unknown", "&ssl%4dode=PREFERRED", "&sslMode=%50REFERRED",
"&sslMode=REQUIRED&sslMode=DISABLED", "&sslMode=VERIFY_CA&sslMode=REQUIRED",
- "&sslMode=%GG"}) {
+ "&sslMode=%GG", "&useSSL=false", "&requireSSL=false", "&useSSL=",
+ "&requireSSL=unknown", "&require%53SL=false"}) {
ConnectionManager manager = fallback();
manager.setStr(options);
assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
@@ -39,7 +40,7 @@ class MariaDbFallbackTlsTest {
@Test void legacyCertificateVerificationRemainsEnabled() {
ConnectionManager manager = fallback();
manager.setStr("&verifyServerCertificate=true&customOption=retained");
- assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA"));
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true"));
}
@Test void ordinaryMysqlNativeMariaAndSslDisabledRemainUnchanged() {
diff --git a/docs/shared-libraries.md b/docs/shared-libraries.md
index c4abb93..fd42aac 100644
--- a/docs/shared-libraries.md
+++ b/docs/shared-libraries.md
@@ -212,9 +212,9 @@ Native MariaDB and custom MariaDB driver overrides retain the MariaDB scheme;
the explicit fallback opt-out remains supported.
For MariaDB configured with `UseSSL=true`, Connector/J fallback requires TLS
-(`sslMode=REQUIRED`) instead of permitting a plaintext downgrade. Stronger
+(`sslMode=REQUIRED` plus `requireSSL=true` for pre-8.0.13 drivers) instead of permitting a plaintext downgrade. Stronger
`VERIFY_CA`/`VERIFY_IDENTITY` modes in `Line` remain supported; weaker or ambiguous
-`sslMode` overrides are rejected. Legacy certificate verification requests remain
+`sslMode` overrides and options disabling `useSSL`/`requireSSL` are rejected. Legacy certificate verification requests remain
verified. This requires encryption but does not claim server identity verification
without the appropriate verified mode. Native MariaDB, ordinary MySQL settings,
and explicitly disabled TLS remain unchanged.
From 43c2f3265481f5ea6e0423c8500d8faabf6b2a4e Mon Sep 17 00:00:00 2001
From: BenCodez <17074231+BenCodez@users.noreply.github.com>
Date: Sat, 3 Oct 2026 10:37:09 -0600
Subject: [PATCH 4/4] Preserve verified TLS modes with legacy Connector/J
---
.../sql/mysql/ConnectionManager.java | 22 ++++-
.../sql/mysql/MariaDbFallbackTlsTest.java | 98 ++++++++++++++++++-
docs/shared-libraries.md | 11 ++-
3 files changed, 122 insertions(+), 9 deletions(-)
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
index 8074215..e2cbe94 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java
@@ -340,7 +340,27 @@ private String requiredMysqlFallbackTls(String extra) {
verifyCertificate = true;
}
}
- return "&sslMode=" + (mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED") + "&requireSSL=true";
+ String requiredMode = mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED";
+ if (verifyCertificate && requiredMode.equals("REQUIRED")) requiredMode = "VERIFY_CA";
+ if (requiredMode.equals("VERIFY_IDENTITY") && !mysqlDriverSupportsSslMode()) {
+ throw new IllegalArgumentException("MariaDB VERIFY_IDENTITY requires Connector/J hostname verification support");
+ }
+ return "&sslMode=" + requiredMode + "&requireSSL=true"
+ + (requiredMode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "");
+ }
+
+ /** Query the driver's supported JDBC properties without opening a connection. */
+ private boolean mysqlDriverSupportsSslMode() {
+ try {
+ java.sql.Driver driver = (java.sql.Driver) Class.forName("com.mysql.cj.jdbc.Driver")
+ .getDeclaredConstructor().newInstance();
+ for (java.sql.DriverPropertyInfo property : driver.getPropertyInfo("jdbc:mysql://localhost/", null)) {
+ if ("sslMode".equals(property.name)) return true;
+ }
+ return false;
+ } catch (ReflectiveOperationException | SQLException | LinkageError failure) {
+ throw new IllegalArgumentException("Unable to determine Connector/J TLS verification support", failure);
+ }
}
private void rejectConflictingPostgreSqlTlsOptions(String extra) {
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
index 7c0a914..3e37925 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java
@@ -11,8 +11,14 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import java.nio.file.Path;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLException;
+import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity;
class MariaDbFallbackTlsTest {
+ @TempDir Path directory;
@Test void sslEnabledFallbackRequiresTls() {
assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED&requireSSL=true"));
}
@@ -21,7 +27,12 @@ class MariaDbFallbackTlsTest {
for (String mode : new String[]{"REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"}) {
ConnectionManager manager = fallback();
manager.setStr("&sslMode=" + mode);
- assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true"));
+ if (mode.equals("VERIFY_IDENTITY") && legacyDriver()) {
+ assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
+ } else {
+ String certificateFlag = mode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "";
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true" + certificateFlag));
+ }
}
}
@@ -40,7 +51,14 @@ class MariaDbFallbackTlsTest {
@Test void legacyCertificateVerificationRemainsEnabled() {
ConnectionManager manager = fallback();
manager.setStr("&verifyServerCertificate=true&customOption=retained");
- assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true"));
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true"));
+ }
+
+ @Test void explicitEncryptionRetainsLegacyCertificateVerification() {
+ ConnectionManager manager = fallback();
+ manager.setStr("&sslMode=REQUIRED&verifyServerCertificate=true");
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith(
+ "&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true"));
}
@Test void ordinaryMysqlNativeMariaAndSslDisabledRemainUnchanged() {
@@ -84,6 +102,76 @@ class MariaDbFallbackTlsTest {
}
}
+ @Test void identityVerificationFailsClosedOnLegacyDriver() {
+ ConnectionManager manager = fallback();
+ manager.setStr("&sslMode=VERIFY_IDENTITY");
+ if (legacyDriver()) {
+ assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
+ } else {
+ assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").contains("sslMode=VERIFY_IDENTITY"));
+ }
+ }
+
+ @Test void verifiedFallbackRejectsUntrustedTlsPeerBeforeAuthentication() throws Exception {
+ var context = HttpTlsIdentity.loadOrCreate(directory.resolve("untrusted-server"), "localhost").serverContext();
+ try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
+ server.setSoTimeout(5000);
+ var worker = Executors.newSingleThreadExecutor();
+ try {
+ var peer = worker.submit(() -> {
+ try (var socket = server.accept()) {
+ socket.setSoTimeout(5000);
+ socket.getOutputStream().write(greeting(true));
+ socket.getOutputStream().flush();
+ byte[] header = socket.getInputStream().readNBytes(4);
+ assertEquals(4, header.length);
+ int length = (header[0] & 255) | ((header[1] & 255) << 8) | ((header[2] & 255) << 16);
+ assertEquals(32, length, "Client must send a bounded SSL request before TLS");
+ byte[] request = socket.getInputStream().readNBytes(length);
+ assertEquals(32, request.length);
+ assertTrue((request[1] & 8) != 0, "CLIENT_SSL must be set");
+ try (SSLSocket tls = (SSLSocket) context.getSocketFactory().createSocket(
+ socket, "localhost", socket.getPort(), true)) {
+ tls.setUseClientMode(false);
+ try {
+ tls.startHandshake();
+ return tls.getInputStream().read();
+ } catch (SSLException rejected) {
+ return -1;
+ }
+ }
+ }
+ });
+ ConnectionManager manager = fallback();
+ manager.setHost(server.getInetAddress().getHostAddress());
+ manager.setPort(Integer.toString(server.getLocalPort()));
+ manager.setStr("&sslMode=VERIFY_CA&verifyServerCertificate=false&enabledTLSProtocols=TLSv1.2&connectTimeout=2000&socketTimeout=2000");
+ Properties properties = new Properties();
+ properties.setProperty("user", "test-user");
+ properties.setProperty("password", "test-only-not-a-secret");
+ SQLException failure = assertThrows(SQLException.class, () ->
+ new com.mysql.cj.jdbc.Driver().connect(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"), properties));
+ assertTrue(certificateFailure(failure), "Expected certificate validation failure, got " + failure);
+ assertEquals(-1, peer.get(5, TimeUnit.SECONDS), "No authentication packet may reach an untrusted TLS peer");
+ } finally {
+ worker.shutdownNow();
+ }
+ }
+ }
+
+ private static boolean certificateFailure(Throwable failure) {
+ for (Throwable current = failure; current != null; current = current.getCause()) {
+ if (current instanceof java.security.cert.CertificateException
+ || current instanceof java.security.cert.CertPathBuilderException
+ || current instanceof java.security.cert.CertPathValidatorException) return true;
+ }
+ return false;
+ }
+
+ private static boolean legacyDriver() {
+ return "8.0.12".equals(com.mysql.cj.jdbc.Driver.class.getPackage().getImplementationVersion());
+ }
+
private static ConnectionManager fallback() {
ConnectionManager manager = new ConnectionManager("localhost", "3306", "test", "", "votes");
manager.setDbType(DbType.MARIADB);
@@ -93,14 +181,16 @@ private static ConnectionManager fallback() {
// Protocol-10 greeting with protocol-41, secure connection and plugin authentication,
// deliberately omitting CLIENT_SSL. No actual database or external network is needed.
- private static byte[] nonTlsGreeting() {
+ private static byte[] nonTlsGreeting() { return greeting(false); }
+
+ private static byte[] greeting(boolean ssl) {
ByteArrayOutputStream payload = new ByteArrayOutputStream();
payload.write(10);
payload.writeBytes("8.0.0-test\0".getBytes(StandardCharsets.US_ASCII));
payload.writeBytes(new byte[]{1, 0, 0, 0});
payload.writeBytes("abcdefgh".getBytes(StandardCharsets.US_ASCII));
payload.write(0);
- int capabilities = 1 | 512 | 8192 | 32768 | 0x80000;
+ int capabilities = 1 | 512 | 8192 | 32768 | 0x80000 | (ssl ? 2048 : 0);
payload.write(capabilities & 255);
payload.write((capabilities >>> 8) & 255);
payload.write(45);
diff --git a/docs/shared-libraries.md b/docs/shared-libraries.md
index 6cad1a8..914df51 100644
--- a/docs/shared-libraries.md
+++ b/docs/shared-libraries.md
@@ -230,9 +230,12 @@ Native MariaDB and custom MariaDB driver overrides retain the MariaDB scheme;
the explicit fallback opt-out remains supported.
For MariaDB configured with `UseSSL=true`, Connector/J fallback requires TLS
-(`sslMode=REQUIRED` plus `requireSSL=true` for pre-8.0.13 drivers) instead of permitting a plaintext downgrade. Stronger
-`VERIFY_CA`/`VERIFY_IDENTITY` modes in `Line` remain supported; weaker or ambiguous
-`sslMode` overrides and options disabling `useSSL`/`requireSSL` are rejected. Legacy certificate verification requests remain
-verified. This requires encryption but does not claim server identity verification
+(`sslMode=REQUIRED` plus `requireSSL=true` for pre-8.0.13 drivers) instead of permitting a plaintext downgrade.
+`VERIFY_CA` also enables the legacy certificate-verification property.
+`VERIFY_IDENTITY` is accepted only when the actual driver advertises modern
+`sslMode` support, and otherwise fails before connecting. Weak or ambiguous
+`sslMode` overrides and options disabling `useSSL`/`requireSSL` are rejected.
+Legacy certificate verification requests remain verified even with an explicit
+`REQUIRED` mode. This requires encryption but does not claim server identity verification
without the appropriate verified mode. Native MariaDB, ordinary MySQL settings,
and explicitly disabled TLS remain unchanged.