JDBC Driver for SQL Server Spoofing Vulnerability
Description
Improper input validation in JDBC Driver for SQL Server allows an unauthorized attacker to perform spoofing over a network.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Improper input validation in the Microsoft JDBC Driver for SQL Server allows an unauthenticated attacker to perform spoofing over a network.
Vulnerability
Overview
CVE-2025-59250 describes an improper input validation vulnerability in the Microsoft JDBC Driver for SQL Server. The root cause lies in insufficient validation of certain inputs processed by the driver, which could allow an attacker to manipulate network communication and impersonate a trusted entity. This issue affects the driver's handling of data during the connection or authentication process, potentially enabling spoofing attacks.
Exploitation
An attacker can exploit this vulnerability over a network without requiring authentication. The attack surface is the JDBC driver's network-facing components, which process data from SQL Server or Azure SQL Database. By sending specially crafted input, an attacker could bypass validation checks and cause the driver to accept spoofed responses or credentials. The vulnerability is classified as high severity (CVSS 8.1) and does not require user interaction or elevated privileges.
Impact
Successful exploitation allows an attacker to perform spoofing, potentially leading to unauthorized access to database connections or data interception. This could compromise the confidentiality and integrity of data transmitted between the application and the database server. The driver is widely used in Java applications connecting to Microsoft SQL Server and Azure SQL Database, making this a significant risk for affected deployments.
Mitigation
Microsoft has addressed this vulnerability in the latest release of the JDBC Driver. Users should upgrade to the most recent version of the driver, as indicated in the changelog [1] and support matrix [3]. The fix is included in the driver's ongoing development, with enhancements such as client certificate authentication [4] and defense-in-depth XML secure processing [1] also being added. No workarounds are documented; upgrading is the recommended action.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
com.microsoft.sqlserver:mssql-jdbcMaven | >= 8.3.0.jre11-preview, < 10.2.4.jre11 | 10.2.4.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 11.2.0.jre11, < 11.2.4.jre11 | 11.2.4.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 12.2.0.jre11, < 12.2.1.jre11 | 12.2.1.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 12.6.0.jre11, < 12.6.5.jre11 | 12.6.5.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 12.8.0.jre11, < 12.8.2.jre11 | 12.8.2.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 12.10.0.jre11, < 12.10.2.jre11 | 12.10.2.jre11 |
com.microsoft.sqlserver:mssql-jdbcMaven | >= 13.2.0.jre11, < 13.2.1.jre11 | 13.2.1.jre11 |
Affected products
8- Microsoft/Microsoft JDBC Driver for SQL Server 10.2v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 11.2v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 12.10v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 12.2v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 12.4v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 12.6v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 12.8v5Range: 1.0.0
- Microsoft/Microsoft JDBC Driver for SQL Server 13.2v5Range: 1.0.0
Patches
19732e1bbc6ecFeature | client certificate authentication (#1284)
16 files changed · +722 −18
azure-pipelines.yml+1 −1 modified@@ -10,7 +10,7 @@ jobs: matrix: SQL-2019: Target_SQL: 'HGS-2k19-01' - Ex_Groups: 'xSQLv15' + Ex_Groups: 'xSQLv15, clientCertAuth' SQL-2012: Target_SQL: 'SQL-2K12-SP3-1' Ex_Groups: 'xSQLv12'
pom.xml+13 −2 modified@@ -51,9 +51,10 @@ xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance NTLM - - - - - - - For tests using NTLM Authentication mode (excluded by default) reqExternalSetup - For tests requiring external setup (excluded by default) + clientCertAuth - - For tests requiring client certificate authentication setup (excluded by default) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default testing enabled with SQL Server 2019 (SQLv15) --> - <excludedGroups>xSQLv15, NTLM, reqExternalSetup</excludedGroups> + <excludedGroups>xSQLv15, NTLM, reqExternalSetup, clientCertAuth</excludedGroups> <!-- Use -preview for preview release, leave empty for official release.--> <releaseExt>-preview</releaseExt> @@ -66,6 +67,8 @@ <osgi.comp.version>5.0.0</osgi.comp.version> <antlr.runtime.version>4.7.2</antlr.runtime.version> <google.gson.version>2.8.6</google.gson.version> + <bouncycastle.bcprov.version>1.64</bouncycastle.bcprov.version> + <bouncycastle.bcpkix.version>1.64</bouncycastle.bcpkix.version> <!-- JUnit Test Dependencies --> <junit.platform.version>[1.3.2, 1.5.2]</junit.platform.version> @@ -119,7 +122,15 @@ <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> - <version>1.64</version> + <version>${bouncycastle.bcprov.version}</version> + <optional>true</optional> + </dependency> + + <!-- dependencies for Client Certificate Authentication --> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <version>${bouncycastle.bcpkix.version}</version> <optional>true</optional> </dependency>
src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java+16 −3 modified@@ -62,6 +62,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; @@ -364,6 +365,7 @@ static final String getTokenName(int tdsTokenType) { static final byte ENCRYPT_ON = 0x01; static final byte ENCRYPT_NOT_SUP = 0x02; static final byte ENCRYPT_REQ = 0x03; + static final byte ENCRYPT_CLIENT_CERT = (byte) 0x80; static final byte ENCRYPT_INVALID = (byte) 0xFF; static final String getEncryptionLevel(int level) { @@ -1597,9 +1599,16 @@ enum SSLHandhsakeState { * Server Host Name for SSL Handshake * @param port * Server Port for SSL Handshake + * @param clientCertificate + * Client certificate path + * @param clientKey + * Private key file path + * @param clientKeyPassword + * Private key file's password * @throws SQLServerException */ - void enableSSL(String host, int port) throws SQLServerException { + void enableSSL(String host, int port, String clientCertificate, String clientKey, + String clientKeyPassword) throws SQLServerException { // If enabling SSL fails, which it can for a number of reasons, the following items // are used in logging information to the TDS channel logger to help diagnose the problem. Provider tmfProvider = null; // TrustManagerFactory provider @@ -1774,13 +1783,16 @@ else if (con.getTrustManagerClass() != null) { if (logger.isLoggable(Level.FINEST)) logger.finest(toString() + " Getting TLS or better SSL context"); + KeyManager[] km = (null != clientCertificate && clientCertificate.length() > 0) ? SQLServerCertificateUtils + .getKeyManagerFromFile(clientCertificate, clientKey, clientKeyPassword) : null; + sslContext = SSLContext.getInstance(sslProtocol); sslContextProvider = sslContext.getProvider(); if (logger.isLoggable(Level.FINEST)) logger.finest(toString() + " Initializing SSL context"); - sslContext.init(null, tm, null); + sslContext.init(km, tm, null); // Got the SSL context. Now create an SSL socket over our own proxy socket // which we can toggle between TDS-encapsulated and raw communications. @@ -6202,7 +6214,8 @@ void writeRPCReaderUnicode(String sName, Reader re, long reLength, boolean bOut, void sendEnclavePackage(String sql, ArrayList<byte[]> enclaveCEKs) throws SQLServerException { if (null != con && con.isAEv2()) { - if (null != sql && !sql.isEmpty() && null != enclaveCEKs && 0 < enclaveCEKs.size() && con.enclaveEstablished()) { + if (null != sql && !sql.isEmpty() && null != enclaveCEKs && 0 < enclaveCEKs.size() + && con.enclaveEstablished()) { byte[] b = con.generateEnclavePackage(sql, enclaveCEKs); if (null != b && 0 != b.length) { this.writeShort((short) b.length);
src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java+38 −0 modified@@ -903,5 +903,43 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * Enclave attestation protocol. */ void setEnclaveAttestationProtocol(String protocol); + + /** + * Returns client certificate path for client certificate authentication. + * + * @return Client certificate path. + */ + String getClientCertificate(); + + /** + * Sets client certificate path for client certificate authentication. + * + * @param certPath + * Client certificate path. + */ + void setClientCertificate(String certPath); + + /** + * Returns Private key file path for client certificate authentication. + * + * @return Private key file path. + */ + String getClientKey(); + + /** + * Sets Private key file path for client certificate authentication. + * + * @param keyPath + * Private key file path. + */ + void setClientKey(String keyPath); + + /** + * Sets the password to be used for Private key provided by the user for client certificate authentication. + * + * @param password + * Private key password. + */ + void setClientKeyPassword(String password); }
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBouncyCastleLoader.java+11 −1 modified@@ -1,12 +1,22 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + package com.microsoft.sqlserver.jdbc; +import java.security.Provider; import java.security.Security; /* * Class that is meant to statically load the BouncyCastle Provider for JDK 8. Hides the call so JDK 11/13 don't have to include the dependency. + * Also loads BouncyCastle provider for PKCS1 private key parsing. */ class SQLServerBouncyCastleLoader { static void loadBouncyCastle() { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + Provider p = new org.bouncycastle.jce.provider.BouncyCastleProvider(); + if (null == Security.getProvider(p.getName())) { + Security.addProvider(p); + } } }
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java+295 −0 added@@ -0,0 +1,295 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; + +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; + + +final class SQLServerCertificateUtils { + + static KeyManager[] getKeyManagerFromFile(String certPath, String keyPath, + String keyPassword) throws IOException, GeneralSecurityException, SQLServerException { + if (keyPath != null && keyPath.length() > 0) { + return readPKCS8Certificate(certPath, keyPath, keyPassword); + } else { + return readPKCS12Certificate(certPath, keyPassword); + } + } + + // PKCS#12 format + private static final String PKCS12_ALG = "PKCS12"; + private static final String SUN_X_509 = "SunX509"; + // PKCS#8 format + private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----"; + private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----"; + private static final String JAVA_KEY_STORE = "JKS"; + private static final String CLIENT_CERT = "client-cert"; + private static final String CLIENT_KEY = "client-key"; + // PKCS#1 format + private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----"; + // PVK format + private static final long PVK_MAGIC = 0xB0B5F11EL; + private static final byte[] RSA2_MAGIC = {82, 83, 65, 50}; + private static final String RC4_ALG = "RC4"; + private static final String RSA_ALG = "RSA"; + + private static KeyManager[] readPKCS12Certificate(String certPath, + String keyPassword) throws NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, UnrecoverableKeyException, KeyStoreException, SQLServerException { + KeyStore keystore = KeyStore.getInstance(PKCS12_ALG); + try { + keystore.load(new FileInputStream(certPath), keyPassword.toCharArray()); + } catch (FileNotFoundException e) { + throw new SQLServerException(SQLServerException.getErrString("R_clientCertError"), null, 0, null); + } + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SUN_X_509); + keyManagerFactory.init(keystore, keyPassword.toCharArray()); + return keyManagerFactory.getKeyManagers(); + } + + private static KeyManager[] readPKCS8Certificate(String certPath, String keyPath, + String keyPassword) throws IOException, GeneralSecurityException, SQLServerException { + Certificate clientCertificate = loadCertificate(certPath); + PrivateKey privateKey = loadPrivateKey(keyPath, keyPassword); + + KeyStore keyStore = KeyStore.getInstance(JAVA_KEY_STORE); + keyStore.load(null, null); + keyStore.setCertificateEntry(CLIENT_CERT, clientCertificate); + keyStore.setKeyEntry(CLIENT_KEY, privateKey, keyPassword.toCharArray(), new Certificate[] {clientCertificate}); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyPassword.toCharArray()); + return kmf.getKeyManagers(); + } + + private static PrivateKey loadPrivateKeyFromPKCS8( + String key) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + StringBuilder sb = new StringBuilder(key); + deleteFirst(sb, PEM_PRIVATE_START); + deleteFirst(sb, PEM_PRIVATE_END); + byte[] formattedKey = Base64.getDecoder().decode(sb.toString().replaceAll("\\s", "")); + + KeyFactory factory = KeyFactory.getInstance(RSA_ALG); + return factory.generatePrivate(new PKCS8EncodedKeySpec(formattedKey)); + } + + private static void deleteFirst(StringBuilder sb, String str) { + int i = sb.indexOf(str); + if (i != -1) { + sb.delete(i, i + str.length()); + } + } + + private static PrivateKey loadPrivateKeyFromPKCS1(String key, + String keyPass) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + SQLServerBouncyCastleLoader.loadBouncyCastle(); + PEMParser pemParser = null; + try { + pemParser = new PEMParser(new StringReader(key)); + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + KeyPair kp; + if (object instanceof PEMEncryptedKeyPair && keyPass != null) { + PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPass.toCharArray()); + kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)); + } else { + kp = converter.getKeyPair((PEMKeyPair) object); + } + return kp.getPrivate(); + } finally { + if (null != pemParser) { + pemParser.close(); + } + } + } + + private static PrivateKey loadPrivateKeyFromPVK(String keyPath, + String keyPass) throws IOException, GeneralSecurityException, SQLServerException { + File f = new File(keyPath); + ByteBuffer buffer = ByteBuffer.allocate((int) f.length()); + try (FileInputStream in = new FileInputStream(f)) { + in.getChannel().read(buffer); + buffer.order(ByteOrder.LITTLE_ENDIAN).rewind(); + + long magic = buffer.getInt() & 0xFFFFFFFFL; + if (PVK_MAGIC != magic) { + SQLServerException.makeFromDriverError(null, magic, SQLServerResource.getResource("R_pvkHeaderError"), + "", false); + } + + buffer.position(buffer.position() + 8); // skip reserved and keytype + boolean encrypted = buffer.getInt() != 0; + int saltLength = buffer.getInt(); + int keyLength = buffer.getInt(); + byte[] salt = new byte[saltLength]; + buffer.get(salt); + + buffer.position(buffer.position() + 8); // skip btype(1b), version(1b), reserved(2b), and keyalg(4b) + + byte[] key = new byte[keyLength - 8]; + buffer.get(key); + + if (encrypted) { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + digest.update(salt); + if (null != keyPass) { + digest.update(keyPass.getBytes()); + } + byte[] hash = digest.digest(); + key = getSecretKeyFromHash(key, hash); + } + + ByteBuffer buff = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN); + buff.position(RSA2_MAGIC.length); // skip the header + + int byteLength = buff.getInt() / 8; + BigInteger publicExponent = BigInteger.valueOf(buff.getInt()); + BigInteger modulus = getBigInteger(buff, byteLength); + BigInteger prime1 = getBigInteger(buff, byteLength / 2); + BigInteger prime2 = getBigInteger(buff, byteLength / 2); + BigInteger primeExponent1 = getBigInteger(buff, byteLength / 2); + BigInteger primeExponent2 = getBigInteger(buff, byteLength / 2); + BigInteger crtCoefficient = getBigInteger(buff, byteLength / 2); + BigInteger privateExponent = getBigInteger(buff, byteLength); + + RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, prime1, + prime2, primeExponent1, primeExponent2, crtCoefficient); + KeyFactory factory = KeyFactory.getInstance(RSA_ALG); + return factory.generatePrivate(spec); + } + } + + private static Certificate loadCertificate(String certificatePem) throws IOException, GeneralSecurityException, SQLServerException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + InputStream certstream = fileToStream(certificatePem); + return certificateFactory.generateCertificate(certstream); + } + + private static PrivateKey loadPrivateKey(String privateKeyPemPath, + String privateKeyPassword) throws GeneralSecurityException, IOException, SQLServerException { + String privateKeyPem = getStringFromFile(privateKeyPemPath); + + if (privateKeyPem.contains(PEM_PRIVATE_START)) { // PKCS#8 format + return loadPrivateKeyFromPKCS8(privateKeyPem); + } else if (privateKeyPem.contains(PEM_RSA_PRIVATE_START)) { // PKCS#1 format + return loadPrivateKeyFromPKCS1(privateKeyPem, privateKeyPassword); + } else { + return loadPrivateKeyFromPVK(privateKeyPemPath, privateKeyPassword); + } + } + + private static boolean startsWithMagic(byte[] b) { + for (int i = 0; i < RSA2_MAGIC.length; i++) { + if (b[i] != RSA2_MAGIC[i]) + return false; + } + return true; + } + + private static byte[] getSecretKeyFromHash(byte[] originalKey, + byte[] keyHash) throws GeneralSecurityException, SQLServerException { + SecretKey key = new SecretKeySpec(keyHash, 0, 16, RC4_ALG); + byte[] decrypted = decryptSecretKey(key, originalKey); + if (startsWithMagic(decrypted)) { + return decrypted; + } + + // Couldn't find magic due to padding, trim the key + Arrays.fill(keyHash, 5, keyHash.length, (byte) 0); + key = new SecretKeySpec(keyHash, 0, 16, RC4_ALG); + decrypted = decryptSecretKey(key, originalKey); + if (startsWithMagic(decrypted)) { + return decrypted; + } + + SQLServerException.makeFromDriverError(null, originalKey, SQLServerResource.getResource("R_pvkParseError"), "", + false); + return null; + } + + private static byte[] decryptSecretKey(SecretKey key, byte[] encoded) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance(key.getAlgorithm()); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(encoded); + } + + private static BigInteger getBigInteger(ByteBuffer buffer, int length) { + // Add an extra bit for signum + byte[] array = new byte[length + 1]; + // Write in reverse because our buffer was set to Little Endian + for (int i = 0; i < length; i++) { + array[array.length - 1 - i] = buffer.get(); + } + return new BigInteger(array); + } + + private static InputStream fileToStream(String fname) throws IOException, SQLServerException { + FileInputStream fis = null; + DataInputStream dis = null; + try { + fis = new FileInputStream(fname); + dis = new DataInputStream(fis); + byte[] bytes = new byte[dis.available()]; + dis.readFully(bytes); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + return bais; + } catch (FileNotFoundException e) { + throw new SQLServerException(SQLServerException.getErrString("R_clientCertError"), null, 0, null); + } finally { + if (null != dis) { + dis.close(); + } + if (null != fis) { + fis.close(); + } + } + } + + private static String getStringFromFile(String filePath) throws IOException { + return new String(Files.readAllBytes(Paths.get(filePath))); + } +}
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java+31 −5 modified@@ -142,6 +142,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private SqlFedAuthToken fedAuthToken = null; private String originalHostNameInCertificate = null; + + private String clientCertificate = null; + private String clientKey = null; + private String clientKeyPassword = ""; final int ENGINE_EDITION_FOR_SQL_AZURE = 5; final int ENGINE_EDITION_FOR_SQL_AZURE_DW = 6; @@ -2021,6 +2025,27 @@ else if (0 == requestedPacketSize) if (null != sPropValue) { activeConnectionProperties.setProperty(sPropKey, sPropValue); } + + sPropKey = SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + activeConnectionProperties.setProperty(sPropKey, sPropValue); + clientCertificate = sPropValue; + } + + sPropKey = SQLServerDriverStringProperty.CLIENT_KEY.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + activeConnectionProperties.setProperty(sPropKey, sPropValue); + clientKey = sPropValue; + } + + sPropKey = SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + activeConnectionProperties.setProperty(sPropKey, sPropValue); + clientKeyPassword = sPropValue; + } FailoverInfo fo = null; String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString(); @@ -2555,7 +2580,7 @@ private void connectHelper(ServerPortPlaceHolder serverInfo, int timeOutSliceInM // If prelogin negotiated SSL encryption then, enable it on the TDS channel. if (TDS.ENCRYPT_NOT_SUP != negotiatedEncryptionLevel) { - tdsChannel.enableSSL(serverInfo.getServerName(), serverInfo.getPortNumber()); + tdsChannel.enableSSL(serverInfo.getServerName(), serverInfo.getPortNumber(), clientCertificate, clientKey, clientKeyPassword); } // We have successfully connected, now do the login. logon takes seconds timeout @@ -2629,7 +2654,7 @@ void Prelogin(String serverName, int portNumber) throws SQLServerException { 0, 0, 0, 0, 0, 0, // - Encryption - - requestedEncryptionLevel, + (null == clientCertificate) ? requestedEncryptionLevel : (byte) (requestedEncryptionLevel | TDS.ENCRYPT_CLIENT_CERT), // TRACEID Data Session (ClientConnectionId + ActivityId) - Initialize to 0 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -4960,7 +4985,7 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ + 4; // AE is always on; // only add lengths of password and username if not using SSPI or requesting federated authentication info - if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) { + if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) { len = len + passwordLen + userBytes.length; } @@ -5038,7 +5063,7 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ tdsWriter.writeShort((short) (tdsLoginRequestBaseLength + dataLen)); tdsWriter.writeShort((short) (0)); - } else if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) { + } else if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) { // User and Password tdsWriter.writeShort((short) (tdsLoginRequestBaseLength + dataLen)); tdsWriter.writeShort((short) (sUser == null ? 0 : sUser.length())); @@ -5130,7 +5155,8 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ // if we are using NTLM or SSPI or fed auth ADAL, do not send over username/password, since we will use SSPI // instead - if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) { + // Also do not send username or password if user is attempting client certificate authentication. + if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) { tdsWriter.writeBytes(userBytes); // Username tdsWriter.writeBytes(passwordBytes); // Password (encrypted) }
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java+30 −0 modified@@ -943,6 +943,36 @@ public void setEnclaveAttestationProtocol(String protocol) { setStringProperty(connectionProps, SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_PROTOCOL.toString(), protocol); } + + @Override + public String getClientCertificate() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(), + SQLServerDriverStringProperty.CLIENT_CERTIFICATE.getDefaultValue()); + } + + @Override + public void setClientCertificate(String certPath) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(), + certPath); + } + + @Override + public String getClientKey() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY.toString(), + SQLServerDriverStringProperty.CLIENT_KEY.getDefaultValue()); + } + + @Override + public void setClientKey(String keyPath) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY.toString(), + keyPath); + } + + @Override + public void setClientKeyPassword(String password) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(), + password); + } /** * Sets a property string value.
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java+12 −2 modified@@ -351,7 +351,10 @@ enum SQLServerDriverStringProperty { SSL_PROTOCOL("sslProtocol", SSLProtocol.TLS.toString()), MSI_CLIENT_ID("msiClientId", ""), KEY_VAULT_PROVIDER_CLIENT_ID("keyVaultProviderClientId", ""), - KEY_VAULT_PROVIDER_CLIENT_KEY("keyVaultProviderClientKey", ""); + KEY_VAULT_PROVIDER_CLIENT_KEY("keyVaultProviderClientKey", ""), + CLIENT_CERTIFICATE("clientCertificate", ""), + CLIENT_KEY("clientKey", ""), + CLIENT_KEY_PASSWORD("clientKeyPassword", ""); private final String name; private final String defaultValue; @@ -598,7 +601,14 @@ public final class SQLServerDriver implements java.sql.Driver { SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(), Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue()), false, - TRUE_FALSE),}; + TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(), + SQLServerDriverStringProperty.CLIENT_CERTIFICATE.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_KEY.toString(), + SQLServerDriverStringProperty.CLIENT_KEY.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(), + SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.getDefaultValue(), false, null), + }; /** * Properties that can only be set by using Properties. Cannot set in connection string
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java+7 −1 modified@@ -254,6 +254,9 @@ protected Object[][] getContents() { {"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."}, {"R_msiClientIdPropertyDescription", "Client Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, + {"R_clientCertificatePropertyDescription", "Client certificate path for client certificate authentication feature."}, + {"R_clientKeyPropertyDescription", "Private key file path for client certificate authentication feature."}, + {"R_clientKeyPasswordPropertyDescription", "Password for private key if the private key is password protected."}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."}, @@ -621,5 +624,8 @@ protected Object[][] getContents() { "Enclave attestation failed, the DH public key signature can't be verified with the enclave public key."}, {"R_AasJWTError", "An error occured when retrieving and validating the JSON web token."}, {"R_AasEhdError", "aas-ehd claim from JWT did not match enclave public key."}, - {"R_VbsRpDataError", "rp_data claim from JWT did not match client nonce."},}; + {"R_VbsRpDataError", "rp_data claim from JWT did not match client nonce."}, + {"R_pvkParseError", "Could not read Private Key from PVK, check the password provided."}, + {"R_pvkHeaderError", "Cannot parse the PVK, PVK file does not contain the correct header."}, + {"R_clientCertError", "Reading client certificate failed. Please verify the location of the certificate."}}; };
src/test/java/com/microsoft/sqlserver/clientcertauth/ClientCertificateAuthenticationTest.java+229 −0 added@@ -0,0 +1,229 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.clientcertauth; + +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Constants; + + +/** + * Tests client certificate authentication feature + * The feature is only supported against SQL Server Linux CU2 or higher. + * + */ +@RunWith(JUnitPlatform.class) +@Tag(Constants.xSQLv12) +@Tag(Constants.xSQLv14) +@Tag(Constants.xAzureSQLDW) +@Tag(Constants.xAzureSQLDB) +@Tag(Constants.clientCertAuth) +public class ClientCertificateAuthenticationTest extends AbstractTest { + + static final String PEM_SUFFIX = ".pem;"; + static final String CER_SUFFIX = ".cer;"; + static final String PVK_SUFFIX = ".pvk;"; + + static final String PKCS1_KEY_SUFFIX = "-pkcs1.key;"; + static final String ENCRYPTED_PKCS1_KEY_SUFFIX = "-encrypted-pkcs1.key;"; + static final String PKCS8_KEY_SUFFIX = "-pkcs8.key;"; + static final String ENCRYPTED_PKCS8_KEY_SUFFIX = "-encrypted-pkcs8.key;"; + static final String PFX_KEY_SUFFIX = ".pfx;"; + static final String ENCRYPTED_PFX_KEY_SUFFIX = "-encrypted.pfx;"; + + /** + * Tests client certificate authentication feature with PKCS1 private key. + * + * @throws Exception + */ + @Test + public void pkcs1Test() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + PKCS1_KEY_SUFFIX; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PKCS1 private key that has been encrypted with a password. + * + * @throws Exception + */ + @Test + public void pkcs1EncryptedTest() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + ENCRYPTED_PKCS1_KEY_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";"; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PKCS8 private key. + * + * @throws Exception + */ + @Test + public void pkcs8Test() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + PKCS8_KEY_SUFFIX; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PKCS8 private key that has been encrypted with a password. + * + * @throws Exception + */ + @Test + public void pkcs8EncryptedTest() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + ENCRYPTED_PKCS8_KEY_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";"; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PFX private key. + * + * @throws Exception + */ + @Test + public void pfxTest() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PFX_KEY_SUFFIX; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PFX private key that has been encrypted with a password. + * + * @throws Exception + */ + @Test + public void pfxEncrytedTest() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + ENCRYPTED_PFX_KEY_SUFFIX + + "clientKeyPassword=" + clientKeyPassword + ";"; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with PVK private key. + * + * @throws Exception + */ + @Test + public void pvkTest() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + CER_SUFFIX + "clientKey=" + + clientKey + PVK_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";"; + try (Connection conn = DriverManager.getConnection(conStr)) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with invalid certificate provided. + * + * @throws Exception + */ + @Test + public void invalidCert() throws Exception { + String conStr = connectionString + ";clientCertificate=invalid_path;" + "clientKeyPassword=" + clientKeyPassword + + ";"; + try (Connection conn = DriverManager.getConnection(conStr)) {} catch (SQLServerException e) { + assertTrue(e.getCause().getMessage().matches(TestUtils.formatErrorMsg("R_clientCertError"))); + } + } + + /** + * Tests client certificate authentication feature with invalid certificate password provided. + * + * @throws Exception + */ + @Test + public void invalidCertPassword() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PFX_KEY_SUFFIX + + "clientKeyPassword=invalid_password;"; + try (Connection conn = DriverManager.getConnection(conStr)) {} catch (SQLServerException e) { + assertTrue(e.getMessage().contains(TestResource.getResource("R_keystorePassword"))); + } + } + + /** + * Tests client certificate authentication feature using a data source. + * + * @throws Exception + */ + @Test + public void testDataSource() throws Exception { + SQLServerDataSource dsLocal = new SQLServerDataSource(); + AbstractTest.updateDataSource(connectionString, dsLocal); + dsLocal.setClientCertificate(clientCertificate + PEM_SUFFIX.substring(0, PEM_SUFFIX.length() - 1)); + dsLocal.setClientKey( + clientKey + ENCRYPTED_PKCS1_KEY_SUFFIX.substring(0, ENCRYPTED_PKCS1_KEY_SUFFIX.length() - 1)); + dsLocal.setClientKeyPassword(clientKeyPassword); + + try (Connection conn = dsLocal.getConnection()) { + assertTrue(conn.isValid(1)); + } + } + + /** + * Tests client certificate authentication feature with encryption turned on. + * + * @throws Exception + */ + @Test + public void testEncryptTrusted() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + PKCS8_KEY_SUFFIX + "encrypt=true;trustServerCertificate=true;"; + try (Connection conn = DriverManager.getConnection(conStr); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt + .executeQuery("SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID"); + rs.next(); + assertTrue(rs.getBoolean(1)); + } + } + + /** + * Tests client certificate authentication feature with encryption turned on, untrusted. + * + * @throws Exception + */ + @Test + public void testEncryptUntrusted() throws Exception { + String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey=" + + clientKey + PKCS8_KEY_SUFFIX + "encrypt=true;trustServerCertificate=false;trustStore=" + + trustStorePath; + try (Connection conn = DriverManager.getConnection(conStr); Statement stmt = conn.createStatement()) { + ResultSet rs = stmt + .executeQuery("SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID"); + rs.next(); + assertTrue(rs.getBoolean(1)); + } + } +}
src/test/java/com/microsoft/sqlserver/jdbc/ActivityIDTest.java+4 −0 modified@@ -1,3 +1,7 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ package com.microsoft.sqlserver.jdbc; import static org.junit.jupiter.api.Assertions.assertEquals;
src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java+2 −1 modified@@ -45,7 +45,8 @@ public void testWithSupportedProtocols(String sslProtocol) throws Exception { // Some older versions of SQLServer might not have all the TLS protocol versions enabled. // Example, if the highest TLS version enabled in the server is TLSv1.1, // the connection will fail if we enable only TLSv1.2 - assertTrue(e.getMessage().contains(TestResource.getResource("R_noProtocolVersion"))); + assertTrue(e.getMessage().contains(TestResource.getResource("R_noProtocolVersion")) + || e.getCause().getCause().getMessage().contains(TestResource.getResource("R_connectionClosed"))); } }
src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java+3 −1 modified@@ -63,6 +63,7 @@ protected Object[][] getContents() { {"R_wrongExceptionMessage", "Wrong exception message"}, {"R_parameterNotDefined", "Parameter {0} was not defined"}, {"R_unexpectedExceptionContent", "Unexpected content in exception message"}, + {"R_connectionClosed", "The connection has been closed"}, {"R_conversionFailed", "Conversion failed when converting {0} to {1} data type"}, {"R_invalidQueryTimeout", "The query timeout value {0} is not valid."}, {"R_skipAzure", "Skipping test case on Azure SQL."}, @@ -183,5 +184,6 @@ protected Object[][] getContents() { {"R_resultSetEmpty", "Result set is empty."}, {"R_AlterAEv2Error", "Alter Column Encryption failed."}, {"R_RichQueryError", "Rich query failed."}, {"R_reqExternalSetup", "External setup for test required."}, {"R_invalidEnclaveSessionFailed", "invalidate enclave session failed."}, - {"R_invalidEnclaveType", "Invalid enclave type {0}."}}; + {"R_invalidEnclaveType", "Invalid enclave type {0}."}, + {"R_keystorePassword", "keystore password was incorrect"}}; }
src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java+23 −0 modified@@ -63,6 +63,12 @@ public abstract class AbstractTest { protected static String[] enclaveServer = null; protected static String[] enclaveAttestationUrl = null; protected static String[] enclaveAttestationProtocol = null; + + protected static String clientCertificate = null; + protected static String clientKey = null; + protected static String clientKeyPassword = ""; + + protected static String trustStorePath = ""; protected static String javaKeyPath = null; protected static String javaKeyAliases = null; @@ -139,6 +145,14 @@ public static void setup() throws Exception { prop = getConfiguredProperty("enclaveAttestationProtocol", null); enclaveAttestationProtocol = null != prop ? prop.split(Constants.SEMI_COLON) : null; + + clientCertificate = getConfiguredProperty("clientCertificate", null); + + clientKey = getConfiguredProperty("clientKey", null); + + clientKeyPassword = getConfiguredProperty("clientKeyPassword", ""); + + trustStorePath = getConfiguredProperty("trustStore", ""); Map<String, SQLServerColumnEncryptionKeyStoreProvider> map = new HashMap<String, SQLServerColumnEncryptionKeyStoreProvider>(); if (null == jksProvider) { @@ -295,6 +309,15 @@ protected static ISQLServerDataSource updateDataSource(String connectionString, case Constants.ENCLAVE_ATTESTATIONPROTOCOL: ds.setEnclaveAttestationProtocol(value); break; + case Constants.CLIENT_CERTIFICATE: + ds.setClientCertificate(value); + break; + case Constants.CLIENT_KEY: + ds.setClientKey(value); + break; + case Constants.CLIENT_KEY_PASSWORD: + ds.setClientKeyPassword(value); + break; default: break; }
src/test/java/com/microsoft/sqlserver/testframework/Constants.java+7 −1 modified@@ -24,6 +24,7 @@ private Constants() {} * xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance * NTLM - - - - - - - For NTLM tests * reqExternalSetup - For tests requiring external setup + * clientCertAuth - - For tests requiring client certificate authentication setup * </pre> */ public static final String xJDBC42 = "xJDBC42"; @@ -36,6 +37,7 @@ private Constants() {} public static final String xAzureSQLMI = "xAzureSQLMI"; public static final String NTLM = "NTLM"; public static final String reqExternalSetup = "reqExternalSetup"; + public static final String clientCertAuth = "clientCertAuth"; public static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); public static final Logger LOGGER = Logger.getLogger("AbstractTest"); @@ -139,7 +141,11 @@ private Constants() {} public static final String ENCLAVE_ATTESTATIONURL = "enclaveAttestationUrl"; public static final String ENCLAVE_ATTESTATIONPROTOCOL = "enclaveAttestationProtocol"; - + + public static final String CLIENT_CERTIFICATE = "CLIENTCERTIFICATE"; + public static final String CLIENT_KEY = "CLIENTKEY"; + public static final String CLIENT_KEY_PASSWORD = "CLIENTKEYPASSWORD"; + public static final String CONFIG_PROPERTIES_FILE = "config.properties"; public enum LOB {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-m494-w24q-6f7wghsaADVISORY
- msrc.microsoft.com/update-guide/vulnerability/CVE-2025-59250ghsavendor-advisorypatchWEB
- nvd.nist.gov/vuln/detail/CVE-2025-59250ghsaADVISORY
- github.com/microsoft/mssql-jdbc/blob/main/CHANGELOG.mdghsaWEB
- github.com/microsoft/mssql-jdbc/commit/9732e1bbc6ec44166fda2cddab31ce1c86c873ddghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2798ghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2800ghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2801ghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2802ghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2803ghsaWEB
- github.com/microsoft/mssql-jdbc/pull/2807ghsaWEB
- learn.microsoft.com/en-us/sql/connect/jdbc/microsoft-jdbc-driver-for-sql-server-support-matrixghsaWEB
News mentions
0No linked articles in our index yet.