Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)

## [10.2.4] Hotfix & Stable Release
### Fixed issues
- **Address a hostname validation vulnerability by securely parsing certificate common names.**
**What was fixed**: Secure hostname validation is enforced by replacing the vulnerable CN parsing logic in SQLServerCertificateUtils.java, preventing spoofing attacks.
**Who benefits**: All users of the SQL Server JDBC driver, especially those relying on TLS for secure connections, benefit from improved certificate validation.

## [10.2.3] HotFix & Stable Release
### Fixed issues
- Fixed incorrect update counts when timeout occurs in batch queries [2024](https://github.com/microsoft/mssql-jdbc/pull/2024)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.2.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
```
The driver can be downloaded from the [Microsoft Download Center](https://go.microsoft.com/fwlink/?linkid=2168495).
Expand All @@ -91,7 +91,7 @@ To get the latest preview version of the driver, add the following to your POM f
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.2.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
```

Expand Down Expand Up @@ -126,7 +126,7 @@ Projects that require either of the two features need to explicitly declare the
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.2.jre17</version>
<version>10.2.4.jre17</version>
<scope>compile</scope>
</dependency>

Expand All @@ -144,7 +144,7 @@ Projects that require either of the two features need to explicitly declare the
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.2.jre17</version>
<version>10.2.4.jre17</version>
<scope>compile</scope>
</dependency>

Expand All @@ -171,7 +171,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.2.jre17</version>
<version>10.2.4.jre17</version>
</dependency>

<dependency>
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

apply plugin: 'java'

version = '10.2.3'
version = '10.2.4'
def jreVersion = ""
def testOutputDir = file("build/classes/java/test")
def archivesBaseName = 'mssql-jdbc'
Expand Down
2 changes: 1 addition & 1 deletion mssql-jdbc_auth_LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MICROSOFT SOFTWARE LICENSE TERMS
MICROSOFT JDBC DRIVER 10.2.3 FOR SQL SERVER
MICROSOFT JDBC DRIVER 10.2.4 FOR SQL SERVER

These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS.

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.3</version>
<version>10.2.4</version>
<packaging>jar</packaging>

<name>Microsoft JDBC Driver for SQL Server</name>
Expand Down
69 changes: 36 additions & 33 deletions src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -1524,38 +1527,34 @@ private final class HostNameOverrideX509TrustManager implements X509TrustManager
this.hostName = hostName.toLowerCase(Locale.ENGLISH);
}

// Parse name in RFC 2253 format
// Returns the common name if successful, null if failed to find the common name.
// The parser tuned to be safe than sorry so if it sees something it cant parse correctly it returns null
private String parseCommonName(String distinguishedName) {
int index;
// canonical name converts entire name to lowercase
index = distinguishedName.indexOf("cn=");
if (index == -1) {
return null;
}
distinguishedName = distinguishedName.substring(index + 3);
// Parse until a comma or end is reached
// Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g
// cn="Foo, bar") however
// RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it
// sees a , within quotes.
for (index = 0; index < distinguishedName.length(); index++) {
if (distinguishedName.charAt(index) == ',') {
break;
/**
* Securely parse the Common Name from an X.509 certificate using RFC2253 format.
* This method prevents DN injection attacks by using LdapName/Rdn parsing.
*
* @param cert X.509 certificate
* @return Common Name from the certificate subject, or null if not found
*/
private String parseCommonNameSecure(X509Certificate cert) {
try {
String subjectDN = cert.getSubjectX500Principal().getName(); // RFC2253 format
LdapName ldapName = new LdapName(subjectDN);

// Iterate through RDNs to find CN
for (Rdn rdn : ldapName.getRdns()) {
if ("CN".equalsIgnoreCase(rdn.getType())) {
return rdn.getValue().toString();
}
}
}
String commonName = distinguishedName.substring(0, index);
// strip any quotes
if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) {
if ('\"' == commonName.charAt(commonName.length() - 1))
commonName = commonName.substring(1, commonName.length() - 1);
else {
// Be safe the name is not ended in " return null so the common Name wont match
commonName = null;
if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext + " No CN found in certificate subject");
}
return null;
} catch (Exception e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning(logContext + " Error parsing certificate: " + e.getMessage());
}
return null;
}
return commonName;
}

private boolean validateServerName(String nameInCert) {
Expand Down Expand Up @@ -1645,17 +1644,21 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) throws
}

private void validateServerNameInCertificate(X509Certificate cert) throws CertificateException {
String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
if (logger.isLoggable(Level.FINER)) {
logger.finer(logContext + " Validating the server name:" + hostName);
logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
}

boolean isServerNameValidated;
String dnsNameInSANCert = "";

// the name in cert is in RFC2253 format parse it to get the actual subject name
String subjectCN = parseCommonName(nameInCertDN);
// Use secure RFC2253 parsing to prevent DN injection attacks
String subjectCN = parseCommonNameSecure(cert);
// X.509 certificate standard requires domain names to be in ASCII.
// Even IDN (Unicode) names will be represented here in Punycode (ASCII).
// Normalize case for comparison using English to avoid case issues like Turkish i.
if (subjectCN != null) {
subjectCN = subjectCN.toLowerCase(Locale.ENGLISH);
}

isServerNameValidated = validateServerName(subjectCN);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
final class SQLJdbcVersion {
static final int major = 10;
static final int minor = 2;
static final int patch = 3;
static final int patch = 4;
static final int build = 0;
/*
* Used to load mssql-jdbc_auth DLL.
Expand Down
2 changes: 1 addition & 1 deletion src/samples/adaptive/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/alwaysencrypted/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/azureactivedirectoryauthentication/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/connections/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/constrained/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/dataclassification/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/datatypes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/resultsets/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
2 changes: 1 addition & 1 deletion src/samples/sparse/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>10.2.0.jre17</version>
<version>10.2.4.jre17</version>
</dependency>
</dependencies>
<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;

import javax.security.auth.x500.X500Principal;

import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
Expand Down Expand Up @@ -135,4 +142,74 @@ public void testValidateServerName() throws Exception {
*/
assertTrue((boolean) method.invoke(hsoObject, "xn--ms.database.windows.net"));
}

// Minimal mock certificate for testing CN parsing security
private static X509Certificate mockCert(final X500Principal subject, final Collection<List<?>> sans) {
return new X509Certificate() {
public X500Principal getSubjectX500Principal() { return subject; }
public Collection<List<?>> getSubjectAlternativeNames() { return sans; }
public X500Principal getIssuerX500Principal() { return subject; }
public java.security.Principal getSubjectDN() { return subject; }
public java.security.Principal getIssuerDN() { return subject; }
public void checkValidity() {}
public void checkValidity(java.util.Date d) {}
public int getVersion() { return 3; }
public java.math.BigInteger getSerialNumber() { return java.math.BigInteger.ONE; }
public java.util.Date getNotBefore() { return new java.util.Date(); }
public java.util.Date getNotAfter() { return new java.util.Date(); }
public byte[] getTBSCertificate() { return new byte[0]; }
public byte[] getSignature() { return new byte[0]; }
public String getSigAlgName() { return ""; }
public String getSigAlgOID() { return ""; }
public byte[] getSigAlgParams() { return null; }
public boolean[] getIssuerUniqueID() { return null; }
public boolean[] getSubjectUniqueID() { return null; }
public boolean[] getKeyUsage() { return null; }
public int getBasicConstraints() { return -1; }
public byte[] getEncoded() { return new byte[0]; }
public void verify(java.security.PublicKey key) {}
public void verify(java.security.PublicKey key, String sigProvider) {}
public java.security.PublicKey getPublicKey() { return null; }
public boolean hasUnsupportedCriticalExtension() { return false; }
public java.util.Set<String> getCriticalExtensionOIDs() { return null; }
public java.util.Set<String> getNonCriticalExtensionOIDs() { return null; }
public byte[] getExtensionValue(String oid) { return null; }
public String toString() { return "Mock Certificate"; }
};
}

@Test
public void testSecureCNParsing_preventsHostnameSpoofing() throws Exception {
// Certificate with spoofed CN via OU attribute: "OU=CN\=target.com, CN=attacker.com"
X500Principal spoofedSubject = new X500Principal("OU=CN\\=target.com, CN=attacker.com");
X509Certificate spoofedCert = mockCert(spoofedSubject, null);

// Set up the HostNameOverrideX509TrustManager object using reflection
TDSChannel tdsc = new TDSChannel(new SQLServerConnection("someConnectionProperty"));
Class<?> hsoClass = Class.forName("com.microsoft.sqlserver.jdbc.TDSChannel$HostNameOverrideX509TrustManager");
Constructor<?> constructor = hsoClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);

// Test rejection against spoofed hostname
Object hsoObjectTargetCom = constructor.newInstance(null, tdsc, null, "target.com");
Method validateMethod = hsoClass.getDeclaredMethod("validateServerNameInCertificate", X509Certificate.class);
validateMethod.setAccessible(true);

// Should throw exception when validating against spoofed hostname
// Note: Method.invoke() wraps exceptions in InvocationTargetException, so we need to unwrap it
try {
validateMethod.invoke(hsoObjectTargetCom, spoofedCert);
throw new AssertionError("Expected CertificateException to be thrown");
} catch (java.lang.reflect.InvocationTargetException e) {
// Unwrap and verify it's a CertificateException
assertTrue("Expected CertificateException but got: " + e.getCause().getClass().getName(),
e.getCause() instanceof CertificateException);
}

// Should pass when validating against real CN
Object hsoObjectAttackerCom = constructor.newInstance(null, tdsc, null, "attacker.com");
assertDoesNotThrow(() -> {
validateMethod.invoke(hsoObjectAttackerCom, spoofedCert);
});
}
}