From ffc389ea7f8e0573b222faed9a791fb8e3c1dda9 Mon Sep 17 00:00:00 2001 From: elsapet Date: Mon, 26 Feb 2024 16:25:55 +0200 Subject: [PATCH] feat(java): missing or permissive ssl hostname verifier (CWE-295) (#231) --- rules/java/lang/ssl_hostname_verifier.yml | 225 ++++++++++++++++++ tests/java/lang/ssl_hostname_verifier/test.js | 18 ++ .../ssl_hostname_verifier/testdata/main.java | 131 ++++++++++ 3 files changed, 374 insertions(+) create mode 100644 rules/java/lang/ssl_hostname_verifier.yml create mode 100644 tests/java/lang/ssl_hostname_verifier/test.js create mode 100644 tests/java/lang/ssl_hostname_verifier/testdata/main.java diff --git a/rules/java/lang/ssl_hostname_verifier.yml b/rules/java/lang/ssl_hostname_verifier.yml new file mode 100644 index 000000000..1cb436034 --- /dev/null +++ b/rules/java/lang/ssl_hostname_verifier.yml @@ -0,0 +1,225 @@ +imports: + - java_shared_lang_instance +patterns: + - pattern: $.ALLOW_ALL_HOSTNAME_VERIFIER; + filters: + - variable: SSL_SOCKET_FACTORY + regex: \A(org\.apache\.http\.conn\.ssl\.)?SSLSocketFactory\z + - pattern: $.$($<...>$); + filters: + - either: + - variable: CALLER + detection: ssl_hostname_verifier_socket_factory + - variable: CALLER + detection: ssl_hostname_verifier_https_url_connection + - variable: METHOD + values: + - setHostnameVerifier + - setDefaultHostnameVerifier + - setSSLHostnameVerifier + - either: + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + detection: ssl_hostname_verifier_allow_all_hostname_verifier + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + detection: ssl_hostname_verifier_allow_all_hostname_verifier_instance + - pattern: $.init($, $<_>, $<_>) + filters: + - variable: SSL_CONTEXT_GET_INSTANCE + detection: ssl_hostname_verifier_ssl_context_get_instance + - variable: "NULL" + detection: ssl_hostname_verifier_null + scope: cursor + - pattern: $.setDisableCNCheck($); + filters: + - variable: TLS_CLIENT_PARAMS + detection: java_shared_lang_instance + scope: cursor + filters: + - variable: JAVA_SHARED_LANG_INSTANCE_TYPE + regex: \A(org\.apache\.cxf\.configuration\.jsse\.)?TLSClientParameters\z + - variable: "TRUE" + detection: ssl_hostname_verifier_true + - pattern: | + new $() { + $$<...>X509Certificate[] getAcceptedIssuers() { + return $; + } + }; + filters: + - variable: X509_TRUST_MANAGER + regex: \A(javax\.net\.ssl\.)?X509TrustManager\z + - variable: NULL_X509_CERTIFICATE + detection: ssl_hostname_verifier_null_x509_cert + - pattern: | + new $() { + $$<...>X509Certificate[] getAcceptedIssuers() { + $[] $ = $; + return $; + } + }; + filters: + - variable: X509_TRUST_MANAGER + regex: \A(javax\.net\.ssl\.)?X509TrustManager\z + - variable: X509_CERTIFICATE_TYPE + regex: \A(java\.security\.cert\.)?X509Certificate\z + - variable: NULL_X509_CERTIFICATE + detection: ssl_hostname_verifier_null_x509_cert + - pattern: | + class $<...>$<_> implements $ { + $$<...>X509Certificate[] getAcceptedIssuers() { + return $; + } + }; + filters: + - variable: X509_TRUST_MANAGER + regex: \A(javax\.net\.ssl\.)?X509TrustManager\z + - variable: NULL_X509_CERTIFICATE + detection: ssl_hostname_verifier_null_x509_cert + - pattern: | + class $<...>$<_> implements $ { + $$<...>X509Certificate[] getAcceptedIssuers() { + $[] $ = $; + return $; + } + }; + filters: + - variable: X509_TRUST_MANAGER + regex: \A(javax\.net\.ssl\.)?X509TrustManager\z + - variable: X509_CERTIFICATE_TYPE + regex: \A(java\.security\.cert\.)?X509Certificate\z + - variable: NULL_X509_CERTIFICATE + detection: ssl_hostname_verifier_null_x509_cert + - pattern: | + new $() { + $$<...>$<_> verify($<...>String $<_>, $<...>$ $<_>) { + return $; + } + }; + filters: + - variable: HOSTNAME_VERIFIER + regex: \A(javax\.net\.ssl\.)?HostnameVerifier\z + - variable: SSL_SESSION + regex: \A(javax\.net\.ssl\.)?SSLSession\z + - variable: "TRUE" + detection: ssl_hostname_verifier_true + - pattern: | + class $<...>$<_> implements $ { + $$<...>$<_> verify($<...>String $<_>, $<...>$ $<_>) { + return $; + } + } + filters: + - variable: HOSTNAME_VERIFIER + regex: \A(javax\.net\.ssl\.)?HostnameVerifier\z + - variable: SSL_SESSION + regex: \A(javax\.net\.ssl\.)?SSLSession\z + - variable: "TRUE" + detection: ssl_hostname_verifier_true +auxiliary: + - id: ssl_hostname_verifier_allow_all_hostname_verifier + patterns: + - pattern: $; + filters: + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + regex: \A(org\.apache\.http\.conn\.ssl\.)?(AllowAllHostnameVerifier|NoopHostnameVerifier|NullHostnameVerifier)\z + - pattern: $.INSTANCE; + filters: + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + regex: \A(org\.apache\.http\.conn\.ssl\.)?(AllowAllHostnameVerifier|NoopHostnameVerifier|NullHostnameVerifier)\z + - pattern: $.ALLOW_ALL_HOSTNAME_VERIFIER; + filters: + - variable: SSL_SOCKET_FACTORY + detection: ssl_hostname_verifier_socket_factory + - pattern: $.ALLOW_ALL_HOSTNAME_VERIFIER; + filters: + - variable: SSL_SOCKET_FACTORY + regex: \A(org\.apache\.http\.conn\.ssl\.)?SSLSocketFactory\z + - id: ssl_hostname_verifier_allow_all_hostname_verifier_instance + patterns: + - pattern: new $(); + filters: + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + detection: ssl_hostname_verifier_allow_all_hostname_verifier + - pattern: ($) new $(); + filters: + - variable: HOSTNAME_VERIFIER_CAST + values: + - HostnameVerifier + - X509HostnameVerifier + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + detection: ssl_hostname_verifier_allow_all_hostname_verifier + - pattern: ($) <$ALLOW_ALL_HOSTNAME_VERIFIER>; + filters: + - variable: ALLOW_ALL_HOSTNAME_VERIFIER + detection: ssl_hostname_verifier_allow_all_hostname_verifier + - variable: HOSTNAME_VERIFIER_CAST + values: + - HostnameVerifier + - X509HostnameVerifier + - id: ssl_hostname_verifier_socket_factory + patterns: + - pattern: $; + filters: + - variable: SSL_SOCKET_FACTORY + detection: java_shared_lang_instance + scope: cursor + filters: + - variable: JAVA_SHARED_LANG_INSTANCE_TYPE + regex: \A(org\.apache\.http\.conn\.ssl\.)?SSLSocketFactory\z + - pattern: $.getSocketFactory(); + filters: + - either: + - variable: CALLER + regex: \A(javax\.net\.ssl\.)?SSLContext\z + - variable: CALLER + regex: \A(org\.apache\.http\.conn\.ssl\.)?SSLSocketFactory\z + - id: ssl_hostname_verifier_https_url_connection + patterns: + - pattern: $; + filters: + - variable: HTTPS_URL_CONNECTION + regex: \A(javax\.net\.ssl\.)?HttpsURLConnection\z + - id: ssl_hostname_verifier_ssl_context_get_instance + patterns: + - pattern: $.getInstance(); + filters: + - variable: SSL_CONTEXT + regex: \A(javax\.net\.ssl\.)?SSLContext\z + - id: ssl_hostname_verifier_null_x509_cert + patterns: + - pattern: new $[]{}; + filters: + - variable: X509_CERT + regex: \A(java\.security\.cert\.)?X509Certificate\z + - pattern: new $[0]; + filters: + - variable: X509_CERT + regex: \A(java\.security\.cert\.)?X509Certificate\z + - pattern: $; + filters: + - variable: "NULL" + detection: ssl_hostname_verifier_null + - id: ssl_hostname_verifier_null + patterns: + - "null;" + - id: ssl_hostname_verifier_true + patterns: + - "true;" +languages: + - java +metadata: + description: "Missing or permissive SSL hostname verifier" + remediation_message: | + ## Description + + It is best security practice to always verify the hostname when establishing a SSL/TLS connection. + Failure to do so puts your application at risk of man-in-the-middle attacks. + + ## Remediations + + ❌ Do not use `ALLOW_ALL_HOSTNAME_VERIFIER` or similar permissive verifiers + + cwe_id: + - 295 + id: java_lang_ssl_hostname_verifier + documentation_url: https://docs.bearer.com/reference/rules/java_lang_ssl_hostname_verifier diff --git a/tests/java/lang/ssl_hostname_verifier/test.js b/tests/java/lang/ssl_hostname_verifier/test.js new file mode 100644 index 000000000..b3766fbbc --- /dev/null +++ b/tests/java/lang/ssl_hostname_verifier/test.js @@ -0,0 +1,18 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("java_lang_ssl_hostname_verifier", () => { + const testCase = "main.java" + + const results = invoke(testCase) + + expect(results.Missing).toEqual([]) + expect(results.Extra).toEqual([]) + }) +}) \ No newline at end of file diff --git a/tests/java/lang/ssl_hostname_verifier/testdata/main.java b/tests/java/lang/ssl_hostname_verifier/testdata/main.java new file mode 100644 index 000000000..d2946f50f --- /dev/null +++ b/tests/java/lang/ssl_hostname_verifier/testdata/main.java @@ -0,0 +1,131 @@ +import javax.net.ssl.X509TrustManager; +import org.apache.http.conn.ssl.SSLSocketFactory; + +SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory(); + +// bearer:expected java_lang_ssl_hostname_verifier +HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; + +// bearer:expected java_lang_ssl_hostname_verifier +HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); + +// bearer:expected java_lang_ssl_hostname_verifier +HttpsURLConnection.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE); + +// bearer:expected java_lang_ssl_hostname_verifier +socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier); +// bearer:expected java_lang_ssl_hostname_verifier +socketFactory.setDefaultHostnameVerifier((HostnameVerifier) new NullHostnameVerifier()); + +public class DummyHostnameVerifier implements HostnameVerifier { + // bearer:expected java_lang_ssl_hostname_verifier + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } +} +HttpsURLConnection.setDefaultHostnameVerifier(new DummyHostnameVerifier()); + +class AllHosts implements HostnameVerifier { + // bearer:expected java_lang_ssl_hostname_verifier + public boolean verify(final String hostname, final SSLSession session) { + return true; + } +} + +public void nullKeyManagerForSSLContext(TrustManager[] trustAllCertificates) { + javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL"); + // bearer:expected java_lang_ssl_hostname_verifier + sc.init(null, tm, null); + + javax.net.ssl.SSLContext sc2 = SSLContext.getInstance("SSL"); + // bearer:expected java_lang_ssl_hostname_verifier + sc2.init(null, tm, null); + + SecureRandom rand = new SecureRandom(); + // bearer:expected java_lang_ssl_hostname_verifier + sc.init(null, tm, rand); +} + +public void disableCommonNameChecking() { + TLSClientParameters tls = new TLSClientParameters(); + tls.setSSLSocketFactory(sslFactory); + // bearer:expected java_lang_ssl_hostname_verifier + tls.setDisableCNCheck(true); + http.setTlsClientParameters(tls); +} + +protected void getAcceptedIssuersOverride() { + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + // bearer:expected java_lang_ssl_hostname_verifier + @Override + public X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[] {}; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + } + }; + + TrustManager[] victimizedManager = new TrustManager[]{ + new X509TrustManager() { + // bearer:expected java_lang_ssl_hostname_verifier + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] myTrustedAnchors = new X509Certificate[0]; + return myTrustedAnchors; + } + } + }; +} + +final static HostnameVerifier NO_VERIFY = new HostnameVerifier() { + // bearer:expected java_lang_ssl_hostname_verifier + public boolean verify(String hostname, SSLSession session) { + return true; + } +}; + +try { + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + // bearer:expected java_lang_ssl_hostname_verifier + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }); +} catch (Exception e) { + e.printStackTrace(); +} + +public class MySocketFactorySubClass extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + + TrustManager tm = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + // bearer:expected java_lang_ssl_hostname_verifier + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + // bearer:expected java_lang_ssl_hostname_verifier + sslContext.init(null, new TrustManager[] { tm }, null); + } +} + +MySocketFactorySubClass socketFactory = new MySocketFactorySubClass(trustStore); +// TODO bearer expected java_lang_ssl_hostname_verifier +socketFactory.setHostnameVerifier(MySocketFactorySubClass.ALLOW_ALL_HOSTNAME_VERIFIER);