Medium severity4.3NVD Advisory· Published Jan 26, 2026· Updated Apr 15, 2026
CVE-2025-14969
CVE-2025-14969
Description
A flaw was found in Hibernate Reactive. When an HTTP endpoint is exposed to perform database operations, a remote client can prematurely close the HTTP connection. This action may lead to leaking connections from the database connection pool, potentially causing a Denial of Service (DoS) by exhausting available database connections.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.hibernate.reactive:hibernate-reactive-coreMaven | < 4.2.1 | 4.2.1 |
Patches
1cd7f104e10de[#2926] Roll back open transactions on close
2 files changed · +76 −31
hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java+6 −0 modified@@ -282,6 +282,12 @@ public interface Log extends BasicLogger { @Message(id = 89, value = "Connection is closed") IllegalStateException connectionIsClosed(); + @Message(id = 90, value = "Live transaction detected while closing the connection: it will be roll backed") + IllegalStateException liveTransactionDetectedOnClose(); + + @Message(id = 91, value = "Can't begin a new transaction as an active transaction is already associated to this connection") + IllegalStateException liveTransactionDetectedOnBeginTransaction(); + // Same method that exists in CoreMessageLogger @LogMessage(level = WARN) @Message(id = 104, value = "firstResult/maxResults specified with collection fetch; applying in memory!" )
hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java+70 −31 modified@@ -36,11 +36,12 @@ import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.SqlResult; -import io.vertx.sqlclient.Transaction; import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.spi.DatabaseMetadata; +import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.rethrow; +import static org.hibernate.reactive.util.impl.CompletionStages.supplyStage; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; /** @@ -62,7 +63,10 @@ public class SqlClientConnection implements ReactiveConnection { private final SqlConnection connection; // The context associated to the connection. We expect the connection to be executed in this context. private final ContextInternal connectionContext; - private Transaction transaction; + + // The close operation could be called multiple times if an error occurs, + // if we execute it every time, we will have several useless messages in the log + private boolean closed = false; SqlClientConnection( SqlConnection connection, @@ -362,52 +366,88 @@ private SqlConnection client() { @Override public CompletionStage<Void> beginTransaction() { - if ( transaction != null ) { - throw new IllegalStateException( "Can't begin a new transaction as an active transaction is already associated to this connection" ); + if ( isTransactionInProgress() ) { + return failedFuture( LOG.liveTransactionDetectedOnBeginTransaction() ); } return connection.begin() .onSuccess( tx -> LOG.tracef( "Transaction started: %s", tx ) ) - .onFailure( v -> LOG.errorf( "Failed to start a transaction: %s", transaction ) ) + .onFailure( throwable -> LOG.errorf( "Failed to start a transaction: %s", throwable.getMessage() ) ) .toCompletionStage() - .thenAccept( this::setTransaction ); + .thenCompose( CompletionStages::voidFuture ); } @Override public CompletionStage<Void> commitTransaction() { - return transaction.commit() - .onSuccess( v -> LOG.tracef( "Transaction committed: %s", transaction ) ) - .onFailure( v -> LOG.errorf( "Failed to commit transaction: %s", transaction ) ) - .toCompletionStage() - .whenComplete( this::clearTransaction ); + return connection.transaction() + .commit() + .onSuccess( v -> LOG.tracef( "Transaction committed: %s", connection.transaction() ) ) + .onFailure( throwable -> LOG.errorf( "Failed to commit transaction: %s", throwable.getMessage() ) ) + .toCompletionStage(); } @Override public CompletionStage<Void> rollbackTransaction() { - return transaction.rollback() - .onFailure( v -> LOG.errorf( "Failed to rollback transaction: %s", transaction ) ) - .onSuccess( v -> LOG.tracef( "Transaction rolled back: %s", transaction ) ) - .toCompletionStage() - .whenComplete( this::clearTransaction ); + if ( isTransactionInProgress() ) { + return connection.transaction() + .rollback() + .onFailure( throwable -> LOG.errorf( "Failed to rollback transaction: %s", throwable.getMessage() ) ) + .onSuccess( v -> LOG.tracef( "Transaction rolled back: %s", connection.transaction() ) ) + .toCompletionStage(); + } + LOG.trace( "No transaction found to roll back" ); + return voidFuture(); } @Override public CompletionStage<Void> close() { - if ( transaction != null ) { - throw new IllegalStateException( "Connection being closed with a live transaction associated to it" ); - } - return connection.close() - .onSuccess( event -> LOG.tracef( "Connection closed: %s", connection ) ) - .onFailure( v -> LOG.errorf( "Failed to close a connection: %s", connection ) ) - .toCompletionStage(); - } - - private void setTransaction(Transaction tx) { - transaction = tx; + // We can probably skip the validation if the connection is already closed...but, you never know + return validateNoTransactionInProgressOnClose() + .handle( CompletionStages::handle ) + .thenCompose( validationHandler -> supplyStage( () -> closed + ? voidFuture().thenAccept( v -> LOG.trace( "Connection already closed" ) ) + : connection.close().toCompletionStage() ) + .handle( CompletionStages::handle ) + .thenCompose( closeConnectionHandler -> { + if ( closeConnectionHandler.hasFailed() ) { + if ( validationHandler.hasFailed() ) { + // Error closing the connection, include the validation error + closeConnectionHandler.getThrowable() + .addSuppressed( validationHandler.getThrowable() ); + } + // Return a failed CompletionStage + return closeConnectionHandler.getResultAsCompletionStage(); + } + if ( !closed ) { + closed = true; + LOG.tracef( "Connection closed: %s", connection ); + } + else { + LOG.tracef( "Connection was already closed: %s", connection ); + } + // Connection closed, return the result of the validation + return validationHandler.getResultAsCompletionStage(); + } ) + ); } - private void clearTransaction(Void v, Throwable x) { - LOG.tracef( "Clearing current transaction instance from connection: %s", transaction ); - transaction = null; + /** + * If there's a transaction open, roll back it and return a failed CompletionStage. + * The validation error is related to closing the connection. + */ + private CompletionStage<Void> validateNoTransactionInProgressOnClose() { + if ( isTransactionInProgress() ) { + return supplyStage( this::rollbackTransaction ) + .handle( CompletionStages::handle ) + .thenCompose( rollbackHandler -> { + final Throwable validationError = LOG.liveTransactionDetectedOnClose(); + if ( rollbackHandler.hasFailed() ) { + // Include the error that happened during rollback + validationError.addSuppressed( rollbackHandler.getThrowable() ); + } + return failedFuture( validationError ); + } ); + } + return voidFuture(); } private static class RowSetResult implements Result { @@ -460,5 +500,4 @@ private static void translateNulls(Object[] paramValues) { } } } - }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-frpp-8pwq-hjrxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-14969ghsaADVISORY
- access.redhat.com/errata/RHSA-2026:1965nvdWEB
- access.redhat.com/security/cve/CVE-2025-14969nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/hibernate/hibernate-reactive/commit/cd7f104e10de918004707ca0e26e3840976f780aghsaWEB
News mentions
0No linked articles in our index yet.