r2dbc-mysql
r2dbc-mysql copied to clipboard
[feature] Add allowMultiQueries Option to MySqlConnectionConfiguration
Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
It is currently not possible to use the allowMultiQueries option in MySqlConnectionConfiguration.
Describe the solution you'd like
Please add an option to specify allowMultiQueries in ConnectionFactoryOptions or MySqlConnectionConfiguration.
Additional context Add any other context or screenshots about the feature request here.
Code Example:
val connectionFactory =
MySqlConnectionFactory.from(
MySqlConnectionConfiguration.builder()
.host(url)
.username(username)
.password(password)
.database(dbname)
.sslMode(SslMode.DISABLED)
.port(port)
.allowMultiQueries(false) // <-
.build(),
)
Even now, multi queries are supported when using client-side prepared queries(default). Do you perhaps not want to allow multi queries? Could you please explain your use case in more detail?
Yes, because I don't want to allow multiple queries. I want to prevent SQL injection vulnerabilities, so it would be nice to have an option.
You're safe from SQL injection when you write static SQL and bind parameters using Statement#bind. However, in case you find any vulnerabilities, please report them via security.
That said, you make a valid point—utilizing the allowMultiQueries flag can help reduce the attack surface.
Thanks!
Thank you.
hello, @jchrys Please review the information below.
Author: brad
feat: add support for CLIENT_MULTI_STATEMENTS capability
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 26299a0..5471c9a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java
@@ -183,6 +183,38 @@ public final class Capability {
public static final Capability DEFAULT = new Capability(ALL_SUPPORTED);
private final long bitmap;
+
+ /**
+ * Returns a new {@link Capability} with the given bit(s) enabled.
+ *
+ * @param flag the bit mask to enable.
+ * @return a new {@link Capability} with the bit(s) set.
+ */
+ public Capability enable(long flag) {
+ return of(this.bitmap | flag);
+ }
+
+ public Capability disable(long flag) {
+ return of(this.bitmap & ~flag);
+ }
+
+ /**
+ * Enables CLIENT_MULTI_STATEMENTS capability.
+ *
+ * @return a new {@link Capability} with MULTI_STATEMENTS enabled.
+ */
+ public Capability enableMultiStatements() {
+ return enable(MULTI_STATEMENTS);
+ }
+
+ /**
+ * Disables CLIENT_MULTI_STATEMENTS capability.
+ *
+ * @return a new {@link Capability} with MULTI_STATEMENTS disabled.
+ */
+ public Capability disableMultiStatements() {
+ return disable(MULTI_STATEMENTS);
+ }
/**
* Checks if the connection is using MariaDB capabilities.
@@ -429,6 +461,8 @@ public final class Capability {
void disableConnectAttributes() {
this.bitmap &= ~CONNECT_ATTRS;
}
+
+ void disableAllowMultiQueries() { this.bitmap &= ~MULTI_STATEMENTS; }
Capability build() {
return of(this.bitmap);
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java
index a7c13c5..4b4c6c0 100644
--- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java
@@ -126,10 +126,11 @@ final class InitFlow {
* @param password the password of the {@code user}.
* @param compressionAlgorithms the list of compression algorithms.
* @param zstdCompressionLevel the zstd compression level.
+ * @param allowMultiQueries whether to enable CLIENT_MULTI_STATEMENTS capability.
* @return a {@link Mono} that indicates the initialization is done, or an error if the initialization failed.
*/
static Mono<Void> initHandshake(Client client, SslMode sslMode, String database, String user,
- @Nullable CharSequence password, Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel) {
+ @Nullable CharSequence password, Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel, boolean allowMultiQueries) {
return client.exchange(new HandshakeExchangeable(
client,
sslMode,
@@ -137,7 +138,8 @@ final class InitFlow {
user,
password,
compressionAlgorithms,
- zstdCompressionLevel
+ zstdCompressionLevel,
+ allowMultiQueries
)).then();
}
@@ -503,6 +505,10 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
private final int zstdCompressionLevel;
+ private Capability loginCapability;
+
+ private final boolean allowMultiQueries;
+
private boolean handshake = true;
private MySqlAuthProvider authProvider;
@@ -513,7 +519,7 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
HandshakeExchangeable(Client client, SslMode sslMode, String database, String user,
@Nullable CharSequence password, Set<CompressionAlgorithm> compressions,
- int zstdCompressionLevel) {
+ int zstdCompressionLevel, boolean allowMultiQueries) {
this.client = client;
this.sslMode = sslMode;
this.database = database;
@@ -521,6 +527,7 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
this.password = password;
this.compressions = compressions;
this.zstdCompressionLevel = zstdCompressionLevel;
+ this.allowMultiQueries = allowMultiQueries;
this.sslCompleted = sslMode == SslMode.TUNNEL;
}
@@ -543,6 +550,19 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
HandshakeRequest request = (HandshakeRequest) message;
Capability capability = initHandshake(request);
+ // Toggle CLIENT_MULTI_STATEMENTS based on configuration
+ boolean serverSupportsMultiStatements = request.getServerCapability().isMultiStatementsAllowed();
+ if (!allowMultiQueries) {
+ capability = capability.disableMultiStatements();
+ } else if (serverSupportsMultiStatements) {
+ capability = capability.enableMultiStatements();
+ } else {
+ capability = capability.disableMultiStatements(); // 서버 미지원: 강제 비활성
+ }
+
+ // Keep the adjusted capability for post-SSL handshake as well
+ this.loginCapability = capability;
+
if (capability.isSslEnabled()) {
emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink);
} else {
@@ -562,7 +582,8 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
sink.complete();
} else if (message instanceof SyntheticSslResponseMessage) {
sslCompleted = true;
- emitNext(createHandshakeResponse(client.getContext().getCapability()), sink);
+ // Use the adjusted capability captured during the initial handshake
+ emitNext(createHandshakeResponse(this.loginCapability != null ? this.loginCapability : client.getContext().getCapability()), sink);
} else if (message instanceof AuthMoreDataMessage) {
AuthMoreDataMessage msg = (AuthMoreDataMessage) message;
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
index 39fb91e..837e627 100644
--- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
@@ -136,6 +136,8 @@ public final class MySqlConnectionConfiguration {
private final boolean tinyInt1isBit;
+ private final boolean allowMultiQueries;
+
private MySqlConnectionConfiguration(
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
@@ -153,7 +155,8 @@ public final class MySqlConnectionConfiguration {
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
@Nullable AddressResolverGroup<?> resolver,
boolean metrics,
- boolean tinyInt1isBit) {
+ boolean tinyInt1isBit,
+ boolean allowMultiQueries) {
this.isHost = isHost;
this.domain = domain;
this.port = port;
@@ -185,6 +188,7 @@ public final class MySqlConnectionConfiguration {
this.resolver = resolver;
this.metrics = metrics;
this.tinyInt1isBit = tinyInt1isBit;
+ this.allowMultiQueries = allowMultiQueries;
}
/**
@@ -327,6 +331,10 @@ public final class MySqlConnectionConfiguration {
boolean isTinyInt1isBit() {
return tinyInt1isBit;
}
+
+ boolean isAllowMultiQueries() {
+ return allowMultiQueries;
+ }
@Override
public boolean equals(Object o) {
@@ -367,7 +375,8 @@ public final class MySqlConnectionConfiguration {
Objects.equals(passwordPublisher, that.passwordPublisher) &&
Objects.equals(resolver, that.resolver) &&
metrics == that.metrics &&
- tinyInt1isBit == that.tinyInt1isBit;
+ tinyInt1isBit == that.tinyInt1isBit &&
+ allowMultiQueries == that.allowMultiQueries;
}
@Override
@@ -382,7 +391,7 @@ public final class MySqlConnectionConfiguration {
loadLocalInfilePath, localInfileBufferSize,
queryCacheSize, prepareCacheSize,
compressionAlgorithms, zstdCompressionLevel,
- loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit);
+ loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit, allowMultiQueries);
}
@Override
@@ -418,7 +427,8 @@ public final class MySqlConnectionConfiguration {
", passwordPublisher=" + passwordPublisher +
", resolver=" + resolver +
", metrics=" + metrics +
- ", tinyInt1isBit=" + tinyInt1isBit;
+ ", tinyInt1isBit=" + tinyInt1isBit +
+ ", allowMultiQueries=" + allowMultiQueries;
}
/**
@@ -521,6 +531,20 @@ public final class MySqlConnectionConfiguration {
private boolean metrics;
private boolean tinyInt1isBit = true;
+
+ private boolean allowMultiQueries = true;
+ /**
+ * Option to enable multiple statements in a single query packet (CLIENT_MULTI_STATEMENTS).
+ * Defaults to {@code true} to preserve current behavior.
+ *
+ * @param allow {@code true} to allow multi-queries, {@code false} to disable.
+ * @return this {@link Builder}
+ * @since 1.4.0
+ */
+ public Builder allowMultiQueries(boolean allow) {
+ this.allowMultiQueries = allow;
+ return this;
+ }
/**
* Builds an immutable {@link MySqlConnectionConfiguration} with current options.
@@ -556,7 +580,8 @@ public final class MySqlConnectionConfiguration {
loadLocalInfilePath,
localInfileBufferSize, queryCacheSize, prepareCacheSize,
compressionAlgorithms, zstdCompressionLevel, loopResources,
- Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit);
+ Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit,
+ allowMultiQueries);
}
/**
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java
index 094674f..189bd35 100644
--- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java
@@ -162,7 +162,8 @@ public final class MySqlConnectionFactory implements ConnectionFactory {
user,
password,
configuration.getCompressionAlgorithms(),
- configuration.getZstdCompressionLevel()
+ configuration.getZstdCompressionLevel(),
+ configuration.isAllowMultiQueries()
).then(InitFlow.initSession(
client,
sessionDb,
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java
index 5905c56..46b8a7f 100644
--- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java
@@ -341,6 +341,14 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr
*/
public static final Option<Boolean> TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit");
+ /**
+ * Option to enable multiple statements in a single query packet (CLIENT_MULTI_STATEMENTS).
+ * Defaults to {@code true}.
+ *
+ * @since 1.4.0
+ */
+ public static final Option<Boolean> ALLOW_MULTI_QUERIES = Option.valueOf("allowMultiQueries");
+
@Override
public ConnectionFactory create(ConnectionFactoryOptions options) {
requireNonNull(options, "connectionFactoryOptions must not be null");
@@ -438,6 +446,8 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr
.to(builder::metrics);
mapper.optional(TINY_INT_1_IS_BIT).asBoolean()
.to(builder::tinyInt1isBit);
+ mapper.optional(ALLOW_MULTI_QUERIES).asBoolean()
+ .to(builder::allowMultiQueries);
return builder.build();
}
diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java
index be48a22..0953635 100644
--- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java
+++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java
@@ -189,6 +189,7 @@ class MySqlConnectionFactoryProviderTest {
.isExactlyInstanceOf(MyHostnameVerifier.class);
assertThatExceptionOfType(MockException.class)
.isThrownBy(() -> configuration.getSsl().customizeSslContext(SslContextBuilder.forClient()));
+ assertThat(configuration.isAllowMultiQueries()).isTrue();
}
@Test
@@ -332,6 +333,43 @@ class MySqlConnectionFactoryProviderTest {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
assertThat(sslContextBuilder)
.isSameAs(configuration.getSsl().customizeSslContext(sslContextBuilder));
+ assertThat(configuration.isAllowMultiQueries()).isTrue();
+ }
+
+ @Test
+ void urlAllowMultiQueries() {
+ MySqlConnectionConfiguration c1 = MySqlConnectionFactoryProvider.setup(
+ ConnectionFactoryOptions.parse("r2dbc:mysql://root@localhost:3306?allowMultiQueries=false"));
+ assertThat(c1.isAllowMultiQueries()).isFalse();
+
+ MySqlConnectionConfiguration c2 = MySqlConnectionFactoryProvider.setup(
+ ConnectionFactoryOptions.parse("r2dbc:mysql://root@localhost:3306?allowMultiQueries=true"));
+ assertThat(c2.isAllowMultiQueries()).isTrue();
+
+ MySqlConnectionConfiguration c3 = MySqlConnectionFactoryProvider.setup(
+ ConnectionFactoryOptions.parse("r2dbc:mysql://root@localhost:3306"));
+ assertThat(c3.isAllowMultiQueries()).isTrue(); // default
+ }
+
+ @Test
+ void programmaticAllowMultiQueries() {
+ MySqlConnectionConfiguration c1 = MySqlConnectionFactoryProvider.setup(
+ ConnectionFactoryOptions.builder()
+ .option(DRIVER, "mysql")
+ .option(HOST, "127.0.0.1")
+ .option(USER, "root")
+ .option(Option.valueOf("allowMultiQueries"), "false")
+ .build());
+ assertThat(c1.isAllowMultiQueries()).isFalse();
+
+ MySqlConnectionConfiguration c2 = MySqlConnectionFactoryProvider.setup(
+ ConnectionFactoryOptions.builder()
+ .option(DRIVER, "mysql")
+ .option(HOST, "127.0.0.1")
+ .option(USER, "root")
+ .option(Option.valueOf("allowMultiQueries"), true)
+ .build());
+ assertThat(c2.isAllowMultiQueries()).isTrue();
}
Could you open a PR with this change?
@tjdskaqks
Hello @jchrys , I reviewed the issue you opened. I attempted to submit a pull request, but I don’t have write access to asyncer-io/r2dbc-mysql. Could you please check the repository’s permissions and grant me access so I can submit the PR?
@tjdskaqks You can create a PR from your own fork. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork