From 3f2e5749cd325050df0e5f923bb24b6d67b3c2a4 Mon Sep 17 00:00:00 2001
From: intrigus
Date: Mon, 1 Jun 2020 23:43:12 +0200
Subject: [PATCH 1/3] [Java] CWE-295 - Incorrect Hostname Verification
---
.../CWE-295/AlwaysTrueHostnameVerifier.java | 6 +
.../CWE-295/AndroidHostnameVerifier.LICENSE | 38 ++++
.../CWE/CWE-295/AndroidHostnameVerifier.java | 20 ++
.../EverythingAcceptingHostnameVerifier.qhelp | 46 +++++
.../EverythingAcceptingHostnameVerifier.ql | 17 ++
.../CWE/CWE-295/HostnameValidation.qll | 183 ++++++++++++++++++
.../CWE-295/IncorrectHostnameVerifier.qhelp | 80 ++++++++
.../CWE/CWE-295/IncorrectHostnameVerifier.ql | 17 ++
.../CWE-295/LocalhostHostnameVerifier.java | 23 +++
.../CWE/CWE-295/PeerHostHostnameVerifier.java | 7 +
.../RawSSLSocketEndpointIdentification.java | 9 +
.../RawSSLSocketHostnameVerifierAndroid.java | 14 ++
...erythingAcceptingHostnameVerifier.expected | 1 +
.../EverythingAcceptingHostnameVerifier.java | 21 ++
.../EverythingAcceptingHostnameVerifier.qlref | 1 +
.../IncorrectHostnameVerifier.expected | 7 +
.../CWE-295/IncorrectHostnameVerifier.java | 100 ++++++++++
.../CWE-295/IncorrectHostnameVerifier.qlref | 1 +
18 files changed, 591 insertions(+)
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/AlwaysTrueHostnameVerifier.java
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.LICENSE
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.java
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.qhelp
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.ql
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.qhelp
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.ql
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/LocalhostHostnameVerifier.java
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/PeerHostHostnameVerifier.java
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketEndpointIdentification.java
create mode 100644 java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketHostnameVerifierAndroid.java
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.expected
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.java
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.qlref
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.expected
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.java
create mode 100644 java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.qlref
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/AlwaysTrueHostnameVerifier.java b/java/ql/src/experimental/Security/CWE/CWE-295/AlwaysTrueHostnameVerifier.java
new file mode 100644
index 000000000000..ccc6f07caaf8
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/AlwaysTrueHostnameVerifier.java
@@ -0,0 +1,6 @@
+HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true; // BAD, verifies anything as correct
+ }
+};
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.LICENSE b/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.LICENSE
new file mode 100644
index 000000000000..0a9014d93e9b
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.LICENSE
@@ -0,0 +1,38 @@
+// Licensed under Apache 2.0 per https://developer.android.com/license
+/*
+Copyright 2020 Android/Android
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+// Create an HostnameVerifier that hardwires the expected hostname.
+// Note that is different than the URL's hostname:
+// example.com versus example.org
+HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ HostnameVerifier hv =
+ HttpsURLConnection.getDefaultHostnameVerifier();
+ return hv.verify("example.com", session); // OKAY-ISH: verify that the certificate matches
+ // example.com and only accept example.com as an alternative to example.org
+ // BETTER: fix the underlying configuration problem that causes example.org to present the certificate for the wrong domain.
+ }
+};
+
+// Tell the URLConnection to use our HostnameVerifier
+URL url = new URL("https://example.org/");
+HttpsURLConnection urlConnection =
+ (HttpsURLConnection)url.openConnection();
+urlConnection.setHostnameVerifier(hostnameVerifier);
+InputStream in = urlConnection.getInputStream();
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.java b/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.java
new file mode 100644
index 000000000000..9a3e7fe7a303
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/AndroidHostnameVerifier.java
@@ -0,0 +1,20 @@
+// Create an HostnameVerifier that hardwires the expected hostname.
+// Note that is different than the URL's hostname:
+// example.com versus example.org
+HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ HostnameVerifier hv =
+ HttpsURLConnection.getDefaultHostnameVerifier();
+ return hv.verify("example.com", session); // OKAY-ISH: verify that the certificate matches
+ // example.com and only accept example.com as an alternative to example.org
+ // BETTER: fix the underlying configuration problem that causes example.org to present the certificate for the wrong domain.
+ }
+};
+
+// Tell the URLConnection to use our HostnameVerifier
+URL url = new URL("https://example.org/");
+HttpsURLConnection urlConnection =
+ (HttpsURLConnection)url.openConnection();
+urlConnection.setHostnameVerifier(hostnameVerifier);
+InputStream in = urlConnection.getInputStream();
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.qhelp b/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.qhelp
new file mode 100644
index 000000000000..899e9335c8a4
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.qhelp
@@ -0,0 +1,46 @@
+
+
+
+
+If a HostnameVerifier always returns true it will not verify the hostname at all.
+This allows an attacker to perform a man-in-the-middle attack on the application therefore breaking any security Transport Layer Security (TLS) gives.
+
+An Attack would look like this:
+1. The program connects to https://example.com.
+2. The attacker intercepts this connection and presents one of their valid certificates they control, for example one from Let's Encrypt.
+3. Java verifies that the certificate has been issued by a trusted certificate authority.
+4. Java verifies that the certificate has been issued for the host example.com, which will fail because the certificate has been issued for malicious.domain.
+5. Java wants to reject the certificate because the hostname does not match. Before doing this it checks whether there exists a HostnameVerifier.
+6. Your HostnameVerifier is called which returns true for any certificate so also for this one.
+7. Java proceeds with the connection since your HostnameVerifier accepted it.
+8. The attacker can now read the data (Man-in-the-middle) your program sends to https://example.com while the program thinks the connection is secure.
+
+
+
+
+
+Do NOT use an unverifying HostnameVerifier!
+
If you use an unverifying verifier to solve a configuration problem with TLS/HTTPS you should solve the configuration problem instead.
+
+If you use an unverifying verifier to connect to a local host you should instead create a self-signed certificate for your local host and import that certificate to the java key store
+If you do not verify the hostname, you allow any attacker to perform a man-in-the-middle attack.
+
+
+
+
+
+
+In the first example, the HostnameVerifier always returns true.
+This allows an attacker to perform a man-in-the-middle attack, because any certificate is accepted despite an incorrect hostname.
+
+
+
+
+
+Android Security Guide for TLS/HTTPS.
+Further Information on Hostname Verification.
+OWASP: CWE-295.
+
+
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.ql b/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.ql
new file mode 100644
index 000000000000..e850d85c3e99
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.ql
@@ -0,0 +1,17 @@
+/**
+ * @name Disabled hostname verification
+ * @description Accepting any certificate as valid for a host allows an attacker to perform a man-in-the-middle attack.
+ * @kind alert
+ * @problem.severity error
+ * @precision high
+ * @id java/everything-accepting-hostname-verifier
+ * @tags security
+ * external/cwe/cwe-295
+ */
+
+import java
+import HostnameValidation
+
+from AlwaysAcceptingVerifyMethod m
+where not m.getDeclaringType() instanceof TestClass
+select m, "$@ accepts any certificate as valid for a host.", m, "This method"
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll b/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
new file mode 100644
index 000000000000..2add152c21db
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
@@ -0,0 +1,183 @@
+import java
+import semmle.code.java.dataflow.TaintTracking
+import semmle.code.java.dataflow.DataFlow
+import semmle.code.java.security.Encryption
+
+/** A method that overrides the `javax.net.ssl.HostnameVerifier.verify` method. */
+class OverridenVerifyMethod extends HostnameVerifierVerify {
+ OverridenVerifyMethod() { this instanceof HostnameVerifierVerify }
+
+ /**
+ * The `hostname` parameter of this method.
+ */
+ Parameter getHostnameParameter() { result = getParameter(0) }
+
+ /**
+ * The `session` parameter of this method.
+ */
+ Parameter getSessionParameter() { result = getParameter(1) }
+}
+
+/** A return statement that returns `true`. */
+class TrueReturnStmt extends ReturnStmt {
+ TrueReturnStmt() { getResult().(CompileTimeConstantExpr).getBooleanValue() = true }
+}
+
+/**
+ * Holds if `m` always returns `true` ignoring any exceptional flow.
+ */
+private predicate alwaysReturnsTrue(OverridenVerifyMethod m) {
+ forex(ReturnStmt rs | rs.getEnclosingCallable() = m | rs instanceof TrueReturnStmt)
+}
+
+/** A method that overrides the `javax.net.ssl.HostnameVerifier.verify` method in a dangerous way. */
+abstract class DangerousVerifyMethod extends OverridenVerifyMethod {
+ /** A `Stmt` that makes this `HostnameVerifier` dangerous. */
+ abstract Stmt getADangerousStmt();
+}
+
+/**
+ * A method that overrides the `javax.net.ssl.HostnameVerifier.verify` method and **always** returns `true`, thus
+ * accepting any certificate despite a hostname mismatch.
+ */
+class AlwaysAcceptingVerifyMethod extends DangerousVerifyMethod {
+ AlwaysAcceptingVerifyMethod() { alwaysReturnsTrue(this) }
+
+ override Stmt getADangerousStmt() {
+ exists(TrueReturnStmt r | r.getEnclosingCallable() = this | result = r)
+ }
+}
+
+/** An access to any method whose name starts with `equals` and which is defined in the `String` class. */
+class StringEqualsMethodAccess extends MethodAccess {
+ StringEqualsMethodAccess() {
+ getMethod().getName().matches("equals%") and
+ getMethod().getDeclaringType() instanceof TypeString
+ }
+
+ // TODO there is probably a better name for "anEqualityPart" but I don't know any right now
+ /** Returns a part of this equality check, i.e. in the case of `foo.equals(bar)` this returns `foo` and `bar`. */
+ Expr getAnEqualityPart() { result = getQualifier() or result = getArgument(0) }
+}
+
+/**
+ * Get the nearest enclosing if statement, if there exists one.
+ * For example consider this case:
+ * ````
+ * if(foo) {
+ * if(bar) {
+ * stmt
+ * }
+ * }
+ * ````
+ * This query would then return the `bar` if statement and **not** the `foo` if statement.
+ */
+private IfStmt getNearestEnclosingIfStmt(Stmt stmt) {
+ //result = getNerestEnclosingIfStmtRecursive(stmt) and result != stmt
+ exists(IfStmt ifStmt |
+ result = ifStmt and
+ (
+ ifStmt.getElse().getAChild*() = stmt and
+ /*
+ * Ensure that the `IfStmt` we found does not contain another `IfStmt`.
+ * If it contains one, the result can not be the nearest enclosing `IfStmt`.
+ */
+
+ not any(ifStmt.getElse().getAChild*()) instanceof IfStmt
+ or
+ ifStmt.getThen().getAChild*() = stmt and
+ /*
+ * Ensure that the `IfStmt` we found does not contain another `IfStmt`.
+ * If it contains one, the result can not be the nearest enclosing `IfStmt`.
+ */
+
+ not any(ifStmt.getThen().getAChild*()) instanceof IfStmt
+ )
+ )
+}
+
+/** The `javax.net.ssl.SSLSession.getPeerHost` method. */
+private class SSLSessionGetPeerHostMethodAccess extends MethodAccess {
+ SSLSessionGetPeerHostMethodAccess() {
+ getMethod().hasName("getPeerHost") and
+ getMethod().getDeclaringType() instanceof SSLSession
+ }
+}
+
+/**
+ * A `TaintTracking::Configuration` where the `source` is a parameter.
+ * This configuration considers any getter on `SSLSession` as being tainted, but ignores any calls to `SSLSession.getPeerHost`.
+ */
+private class ParameterTaintTracking extends TaintTracking::Configuration {
+ ParameterTaintTracking() { this = "ParameterTaintTracking" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof DataFlow::ParameterNode }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof DataFlow::ExprNode }
+
+ override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
+ node1.asExpr().getType() instanceof SSLSession and
+ exists(MethodAccess ma | ma.getMethod().getName().matches("get%") | node2.asExpr() = ma)
+ }
+
+ override predicate isSanitizer(DataFlow::Node node) {
+ node.asExpr() instanceof SSLSessionGetPeerHostMethodAccess
+ }
+}
+
+/** Holds if `e` is derived from `parameter`. This is approximated by checking whether `e` gets tainted by `parameter`. */
+private predicate isDerivedFromParameter(Parameter parameter, Expr e) {
+ exists(ParameterTaintTracking t, DataFlow::ExprNode sink, DataFlow::ParameterNode source |
+ source = DataFlow::parameterNode(parameter) and sink = DataFlow::exprNode(e)
+ |
+ t.hasFlow(source, sink)
+ )
+}
+
+/**
+ * A (dangerous) `StringEqualsMethodAccess` where the equality does:
+ * 1. not depend on a expression that is derived from the `session` parameter of the `verify` method.
+ * 2. depend on a expression that is derived from the `hostname` parameter of the `verify` method.
+ */
+class SSLSessionParameterIgnoringStringEqualsMethodAccess extends StringEqualsMethodAccess {
+ SSLSessionParameterIgnoringStringEqualsMethodAccess() {
+ exists(Expr hostname, Expr other, Parameter hostnameParameter, Parameter sessionParameter |
+ hostnameParameter = getEnclosingCallable().(OverridenVerifyMethod).getHostnameParameter() and
+ sessionParameter = getEnclosingCallable().(OverridenVerifyMethod).getSessionParameter() and
+ getAnEqualityPart() = hostname and
+ getAnEqualityPart() = other and
+ hostname != other
+ |
+ not isDerivedFromParameter(sessionParameter, other) and
+ isDerivedFromParameter(hostnameParameter, hostname)
+ )
+ }
+}
+
+/**
+ * A `verify` method that does not verify the `hostname` correctly. It is incorrect
+ * because it does not validate the `hostname` against any expression that is derived from the `session` parameter of the `verify` method.
+ */
+class IncorrectHostnameVerifyMethod extends DangerousVerifyMethod {
+ Stmt dangerousStmt;
+
+ IncorrectHostnameVerifyMethod() {
+ /* We return `true` based on an if statement that verifies the `hostname` incorrectly.*/
+ exists(TrueReturnStmt r, IfStmt i |
+ getBody().getAChild*() = r and i = getNearestEnclosingIfStmt(r)
+ |
+ i.getCondition().getAChildExpr*() instanceof
+ SSLSessionParameterIgnoringStringEqualsMethodAccess and
+ dangerousStmt = i
+ )
+ or
+ /* We return the result of an `equals` call that verifies the `hostname` incorrectly.*/
+ exists(ReturnStmt r | getBody().getAChild*() = r |
+ not r.getResult() instanceof BooleanLiteral and
+ r.getResult().getAChildExpr*() instanceof SSLSessionParameterIgnoringStringEqualsMethodAccess and
+ dangerousStmt = r
+ )
+ }
+
+ override Stmt getADangerousStmt() { result = dangerousStmt }
+}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.qhelp b/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.qhelp
new file mode 100644
index 000000000000..b4da1b8feaf6
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.qhelp
@@ -0,0 +1,80 @@
+
+
+
+
+If a HostnameVerifier incorrectly verifies the hostname it might not verify the hostname at all.
+This allows an attacker to perform a man-in-the-middle attack on the application therefore breaking any security Transport Layer Security (TLS) gives.
+
+
+
+
+
+Do NOT use an incorrect HostnameVerifier!
+A HostnameVerifier is typically used for either an URL/HttpsURLConnection or a raw SSLSocket.
+A raw SSLSocket does not do any hostname verification so a working hostname verification is important.
+When you use an URL/HttpsURLConnection:
+
+
If you use a verifier to solve a configuration problem with TLS/HTTPS you should solve the configuration problem instead.
+
+If you use a verifier to connect to a local host you should instead create a self-signed certificate for your local host and import that certificate to the java key store
+
+
+When you use a raw SSLSocket:
+
+
Use SSLParameters.setEndpointIdentificationAlgorithm("HTTPS") to enable hostname verification. This is available starting with Java 7 and Android Api Level 24.
+Before Android Api Level 24 you should use the HostnameVerifier from HttpsURLConnection.getDefaultHostnameVerifier() to verify the hostname.
+If you do not verify the hostname, you allow any attacker to perform a man-in-the-middle attack.
+
+
+
+
+
+
+In the first example, the HostnameVerifier verifies the hostname against session.getPeerHost().
+This allows an attacker to perform a man-in-the-middle attack, because getPeerHost() is not authenticated and will always be equal to the hostname.
+
+
+
+
+In the second example, the HostnameVerifier verifies the hostname against different local hosts.
+Comparing hostname against localhost does not verify the certificate. In theory traffic to localhost
+should never leave the host. In practice this may not always be the case due to misconfigurations.
+Comparing hostname against 127.0.0.1 or ::1 does not verify the certificate. These adresses are loopback adresses and traffic
+MUST never leave the host. So this SHOULD be safe, but nevertheless it would be better to use a proper self-signed certificate.
+
+
+
+
+In the third example, we want to connect to the example.org host, but the host presents a certificate for example.com.
+Assuming that example.org and example.com are under our control (and only then this is ok) we can use the default HostnameVerifier
+to verify that the certificate belongs to example.com.
+Note that you should really fix the configuration problem that causes example.org to present the certificate for example.com instead of using this method.
+This method **only** works on Android, on (OpenJDK) Java the default HostnameVerifier will always return false.
+On (OpenJDK) Java you can use this implementation of a working
+hostname verifier instead of the default HostnameVerifier.
+
+
+
+
+In the fourth example, we properly use a raw SSLSocket with enabled SSLParameters.setEndpointIdentificationAlgorithm("HTTPS"), thus verifying hostnames correctly.
+Note that this method is only available with Java 7 and Android Api Level 24.
+The next example shows an alternative for Android Api Levels before 24.
+
+
+
+
+In the fifth example, we properly use a raw SSLSocket and verify the hostname using the HostnameVerifier from HttpsURLConnection.getDefaultHostnameVerifier().
+Note that this is also works on Android Api Levels before 24.
+But also note that this will **not** work on (OpenJDK) Java, because the default HostnameVerifier will always return false.
+
+
+
+
+
+Android Security Guide for TLS/HTTPS.
+Further Information on Hostname Verification.
+OWASP: CWE-295.
+
+
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.ql b/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.ql
new file mode 100644
index 000000000000..3d2ea747defa
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.ql
@@ -0,0 +1,17 @@
+/**
+ * @name Incorrect hostname verification
+ * @description Incorrectly verifying a hostname allows an attacker to perform a man-in-the-middle attack.
+ * @kind alert
+ * @problem.severity error
+ * @precision high
+ * @id java/incorrect-hostname-verifier
+ * @tags security
+ * external/cwe/cwe-295
+ */
+
+import java
+import HostnameValidation
+
+from IncorrectHostnameVerifyMethod m, Stmt dangerousStmt
+where dangerousStmt = m.getADangerousStmt() and not m.getDeclaringType() instanceof TestClass
+select dangerousStmt, "$@ incorrectly verifies the hostname.", dangerousStmt, "This statement"
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/LocalhostHostnameVerifier.java b/java/ql/src/experimental/Security/CWE/CWE-295/LocalhostHostnameVerifier.java
new file mode 100644
index 000000000000..11bbcab00970
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/LocalhostHostnameVerifier.java
@@ -0,0 +1,23 @@
+HostnameVerifier localHostHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ if (hostname.equals("localhost")) { // BAD, does not verify the certificate. In theory traffic to
+ // "localhost" should never leave the host. In practice this may not
+ // always be the case due to misconfigurations.
+ return true;
+ } else if (hostname.equals("127.0.0.1")) { // BAD, does not verify the certificate [Debatable Security
+ // Impact]. "127.0.0.1" is the IPv4 loopback adress and traffic
+ // MUST never leave the host. So this SHOULD be safe, but
+ // nevertheless it would be better to use a proper self-signed
+ // certificate.
+ return true;
+ } else if (hostname.equals("::1")) { // BAD, does not verify the certificate [Debatable Security Impact].
+ // "::1" is the IPv6 loopback adress and traffic MUST never leave
+ // the host. So this SHOULD be safe, but nevertheless it would be
+ // better to use a proper self-signed certificate.
+ return true;
+ }
+ return false;
+
+ }
+};
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/PeerHostHostnameVerifier.java b/java/ql/src/experimental/Security/CWE/CWE-295/PeerHostHostnameVerifier.java
new file mode 100644
index 000000000000..2c1312a1e05c
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/PeerHostHostnameVerifier.java
@@ -0,0 +1,7 @@
+HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return hostname.equals(session.getPeerHost()); // BAD, getPeerHost() is not authenticated
+ // and will always be equal to the hostname, therefore trusting ALL hostnames
+ }
+};
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketEndpointIdentification.java b/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketEndpointIdentification.java
new file mode 100644
index 000000000000..66b5dc1680a1
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketEndpointIdentification.java
@@ -0,0 +1,9 @@
+// Open SSLSocket directly to example.org.
+SocketFactory sf = SSLSocketFactory.getDefault();
+SSLSocket socket = (SSLSocket) sf.createSocket("example.org", 443);
+SSLParameters sslParams = new SSLParameters();
+sslParams.setEndpointIdentificationAlgorithm("HTTPS"); // GOOD, enable hostname verification using the `HTTPS`
+ // verification algorithm.
+socket.setSSLParameters(sslParams);
+
+// Hostname verification has been performed and we can proceed.
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketHostnameVerifierAndroid.java b/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketHostnameVerifierAndroid.java
new file mode 100644
index 000000000000..a430087141da
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/RawSSLSocketHostnameVerifierAndroid.java
@@ -0,0 +1,14 @@
+// Open SSLSocket directly to example.org.
+SocketFactory sf = SSLSocketFactory.getDefault();
+SSLSocket socket = (SSLSocket) sf.createSocket("example.org", 443);
+HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
+SSLSession s = socket.getSession();
+
+// Verify that the certificate matches the hostname.
+if (!hv.verify(s.getPeerHost(), s)) { // GOOD
+ // We use the default hostname verifier from Android to verify that the
+ // certificate matches.
+ throw new SSLHandshakeException("Expected <" + s.getPeerHost() + ">, but " + "found " + s.getPeerPrincipal());
+}
+
+// Hostname verification has been performed and we can proceed.
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.expected b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.expected
new file mode 100644
index 000000000000..f2f92bd65bef
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.expected
@@ -0,0 +1 @@
+| EverythingAcceptingHostnameVerifier.java:7:18:7:23 | verify | $@ accepts any certificate as valid for a host. | EverythingAcceptingHostnameVerifier.java:7:18:7:23 | verify | This method |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.java b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.java
new file mode 100644
index 000000000000..fc491b81fd0f
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.java
@@ -0,0 +1,21 @@
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public class EverythingAcceptingHostnameVerifier {
+ HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true; // BAD, always returns true, accepts everything
+ }
+ };
+
+ HostnameVerifier hostnameVerifier2 = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ if (hostname.equals("localhost")) {
+ return true; // BAD [Should not be detected by `EverythingAcceptingHostnameVerifier.ql`]
+ }
+ return false;
+ }
+ };
+}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.qlref b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.qlref
new file mode 100644
index 000000000000..ea321c0ba9fa
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/EverythingAcceptingHostnameVerifier.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-295/EverythingAcceptingHostnameVerifier.ql
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.expected b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.expected
new file mode 100644
index 000000000000..1e784ba0c564
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.expected
@@ -0,0 +1,7 @@
+| EverythingAcceptingHostnameVerifier.java:15:4:15:36 | stmt | $@ incorrectly verifies the hostname. | EverythingAcceptingHostnameVerifier.java:15:4:15:36 | stmt | This statement |
+| IncorrectHostnameVerifier.java:9:4:9:49 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:9:4:9:49 | stmt | This statement |
+| IncorrectHostnameVerifier.java:18:4:18:40 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:18:4:18:40 | stmt | This statement |
+| IncorrectHostnameVerifier.java:26:4:26:36 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:26:4:26:36 | stmt | This statement |
+| IncorrectHostnameVerifier.java:30:11:30:43 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:30:11:30:43 | stmt | This statement |
+| IncorrectHostnameVerifier.java:36:11:36:37 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:36:11:36:37 | stmt | This statement |
+| IncorrectHostnameVerifier.java:97:4:97:46 | stmt | $@ incorrectly verifies the hostname. | IncorrectHostnameVerifier.java:97:4:97:46 | stmt | This statement |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.java b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.java
new file mode 100644
index 000000000000..995cc56ca929
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.java
@@ -0,0 +1,100 @@
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.HttpsURLConnection;
+
+public class IncorrectHostnameVerifier {
+ HostnameVerifier peerHostHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return hostname.equals(session.getPeerHost()); // BAD, getPeerHost() is not authenticated and will always be
+ // equal to the hostname, therefore trusting ALL hostnames.
+ }
+ };
+
+ HostnameVerifier peerHostHostnameVerifier2 = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ String peerHostname = session.getPeerHost();
+ return hostname.equals(peerHostname); // BAD, getPeerHost() is not authenticated and will always be equal to
+ // the hostname, therefore trusting ALL hostnames.
+ }
+ };
+
+ HostnameVerifier localHostHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ if (hostname.equals("localhost")) { // BAD, does not verify the certificate. In theory traffic to
+ // "localhost" should never leave the host. In practice this may not
+ // always be the case due to misconfigurations.
+ return true;
+ } else if (hostname.equals("127.0.0.1")) { // BAD, does not verify the certificate [Debatable Security
+ // Impact]. "127.0.0.1" is the IPv4 loopback adress and traffic
+ // MUST never leave the host. So this SHOULD be safe, but
+ // nevertheless it would be better to use a proper self-signed
+ // certificate.
+ return true;
+ } else if (hostname.equals("::1")) { // BAD, does not verify the certificate [Debatable Security Impact].
+ // "::1" is the IPv6 loopback adress and traffic MUST never leave
+ // the host. So this SHOULD be safe, but nevertheless it would be
+ // better to use a proper self-signed certificate.
+ return true;
+ }
+ return false;
+
+ }
+ };
+
+ HostnameVerifier delegatingHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
+ if (hostname.equals("example.com")) {
+ return verifier.verify("example.org", session); // "GOOD-ISH", if and only if this holds:
+ // We want to make a connection to `https://example.com`, but the webserver is
+ // misconfigured and wrongly returns the certificate for `https://example.org`.
+ // IF we control BOTH example.com and example.org we accept an certificate for
+ // `example.org` as valid for `example.com`.
+ // NOTE: It's STRONGLY suggested to fix the certificate problem instead.
+ // NOTE: This will only work on Android, on (OpenJDK) Java the default
+ // `HostnameVerifier` always returns false!
+ }
+ return false;
+ }
+ };
+
+ HostnameVerifier externalMethodHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return checkHostname(hostname); // BAD [NOT DETECTED], only the implementation of `verify` method is
+ // checked. If the `verify` method calls other methods we can not reason
+ // about them. A simple implementation would be to detect that the external
+ // method only checks `hostname` and not a value derived from `session`.
+ }
+
+ private boolean checkHostname(String hostname) {
+ return hostname.equals("example.com"); // BAD [NOT DETECTED], does not verify the certificate.
+ }
+ };
+
+ HostnameVerifier externalMethodHostnameVerifier2 = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return check(hostname, session); // BAD [NOT DETECTED], only the implementation of the `verify` method is
+ // checked. If the `verify` method calls other methods we can not reason
+ // about them. The simple implementation from above
+ // (`externalMethodHostnameVerifier`) (if implemented) should not detect
+ // this.
+ }
+
+ private boolean check(String hostname, SSLSession session) {
+ return hostname.equals("example.com"); // BAD [NOT DETECTED], does not verify the certificate.
+ }
+ };
+
+ HostnameVerifier hostSpecificHostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return hostname.equals("host.example.org"); // BAD, does not verify the certificate for `host.example.org`.
+ }
+ };
+}
diff --git a/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.qlref b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.qlref
new file mode 100644
index 000000000000..aa730dfa296c
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-295/IncorrectHostnameVerifier.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-295/IncorrectHostnameVerifier.ql
From 3f1d01df0d7afa50aaa4727b292cc3ff2ad781fd Mon Sep 17 00:00:00 2001
From: intrigus
Date: Mon, 1 Jun 2020 23:43:48 +0200
Subject: [PATCH 2/3] [Java] Add SSLSession Type
---
java/ql/src/semmle/code/java/security/Encryption.qll | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/java/ql/src/semmle/code/java/security/Encryption.qll b/java/ql/src/semmle/code/java/security/Encryption.qll
index c9d2b6e4ddb7..760c2c0fb6a5 100644
--- a/java/ql/src/semmle/code/java/security/Encryption.qll
+++ b/java/ql/src/semmle/code/java/security/Encryption.qll
@@ -21,6 +21,10 @@ class SSLSocketFactory extends RefType {
SSLSocketFactory() { this.hasQualifiedName("javax.net.ssl", "SSLSocketFactory") }
}
+class SSLSession extends RefType {
+ SSLSession() { hasQualifiedName("javax.net.ssl", "SSLSession") }
+}
+
class SSLContext extends RefType {
SSLContext() { hasQualifiedName("javax.net.ssl", "SSLContext") }
}
From f5d49b20b4dbd4ecdb2b6cfbbac84828bcdbd946 Mon Sep 17 00:00:00 2001
From: intrigus
Date: Wed, 24 Jun 2020 23:49:07 +0200
Subject: [PATCH 3/3] Temporary fix
---
.../CWE/CWE-295/HostnameValidation.qll | 42 +++++++++++++------
1 file changed, 30 insertions(+), 12 deletions(-)
diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll b/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
index 2add152c21db..96e44f885407 100644
--- a/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
+++ b/java/ql/src/experimental/Security/CWE/CWE-295/HostnameValidation.qll
@@ -108,29 +108,47 @@ private class SSLSessionGetPeerHostMethodAccess extends MethodAccess {
* A `TaintTracking::Configuration` where the `source` is a parameter.
* This configuration considers any getter on `SSLSession` as being tainted, but ignores any calls to `SSLSession.getPeerHost`.
*/
-private class ParameterTaintTracking extends TaintTracking::Configuration {
- ParameterTaintTracking() { this = "ParameterTaintTracking" }
+/*
+ * private class ParameterTaintTracking extends TaintTracking::Configuration {
+ * ParameterTaintTracking() { this = "ParameterTaintTracking" }
+ */
- override predicate isSource(DataFlow::Node source) { source instanceof DataFlow::ParameterNode }
+/*override */ predicate isSource(DataFlow::Node source) {
+ source instanceof DataFlow::ParameterNode
+}
- override predicate isSink(DataFlow::Node sink) { sink instanceof DataFlow::ExprNode }
+/*override */ predicate isSink(DataFlow::Node sink) { sink instanceof DataFlow::ExprNode }
- override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
- node1.asExpr().getType() instanceof SSLSession and
- exists(MethodAccess ma | ma.getMethod().getName().matches("get%") | node2.asExpr() = ma)
- }
+/*override */ predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
+ exists(MethodAccess ma |
+ ma.getMethod().getName().matches("get%") and
+ ma.getMethod().getDeclaringType() instanceof SSLSession and
+ not ma instanceof SSLSessionGetPeerHostMethodAccess
+ |
+ node2.asExpr() = ma and
+ ma.getQualifier() = node1.asExpr()
+ )
+}
- override predicate isSanitizer(DataFlow::Node node) {
- node.asExpr() instanceof SSLSessionGetPeerHostMethodAccess
+class TaintedQualifierTaintStep extends TaintTracking::AdditionalTaintStep {
+ override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
+ isAdditionalTaintStep(node1, node2)
}
}
+/*
+ * override predicate isSanitizer(DataFlow::Node node) {
+ * node.asExpr() instanceof SSLSessionGetPeerHostMethodAccess
+ * }
+ * }
+ */
+
/** Holds if `e` is derived from `parameter`. This is approximated by checking whether `e` gets tainted by `parameter`. */
private predicate isDerivedFromParameter(Parameter parameter, Expr e) {
- exists(ParameterTaintTracking t, DataFlow::ExprNode sink, DataFlow::ParameterNode source |
+ exists(DataFlow::ExprNode sink, DataFlow::ParameterNode source |
source = DataFlow::parameterNode(parameter) and sink = DataFlow::exprNode(e)
|
- t.hasFlow(source, sink)
+ TaintTracking::localTaint(source, sink)
)
}