官方的 Oracle SSLSocketClient.java 演示代码是否存在安全问题?

huangapple go评论97阅读模式
英文:

Is the official Oracle SSLSocketClient.java demo code insecure?

问题

从这个链接提供了一个关于SSLSocketClient.java的演示:

import java.net.*;
import java.io.*;
import javax.net.ssl.*;

public class SSLSocketClient {
    public static void main(String[] args) throws Exception {
        try {
            SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
            SSLSocket socket = (SSLSocket) factory.createSocket("www.verisign.com", 443);

            socket.startHandshake();

            PrintWriter out = new PrintWriter(
                                  new BufferedWriter(
                                  new OutputStreamWriter(
                                  socket.getOutputStream())));

            out.println("GET / HTTP/1.0");
            out.println();
            out.flush();

            if (out.checkError())
                System.out.println("SSLSocketClient: java.io.PrintWriter error");

            BufferedReader in = new BufferedReader(
                                    new InputStreamReader(
                                    socket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null)
                System.out.println(inputLine);

            in.close();
            out.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我有两个问题:

  1. 根据这份官方文档,如果我们使用原始的SSLSocketFactory而不是HttpsURLConnection,握手过程中不会强制执行主机名验证。因此,主机名验证应该在握手过程中手动完成。

    当使用原始的SSLSocket和SSLEngine类时,在发送任何数据之前,应始终检查对等方的凭据。SSLSocket和SSLEngine类不会自动验证URL中的主机名是否与对等方凭据中的主机名匹配。如果未验证主机名,则应用可能会受到URL欺骗的攻击。自JDK 7以来,可以在SSL/TLS握手期间处理端点标识/验证过程。请参阅SSLParameters.getEndpointIdentificationAlgorithm方法。

    这是否意味着该演示不安全?

  2. 我看到在Java 7中有一种添加主机名验证的解决方案:

    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    sslSocket.setSSLParameters(sslParams);
    

    当算法指定为"HTTPS"时,握手将验证主机名。否则(仅使用原始的SSLSockeFactory,算法为空),根本不会触发主机名验证。我对以下修复方式感到好奇:

    SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
    SSLSocket socket = (SSLSocket) factory.createSocket("www.verisign.com", 443);
    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
    if (!hv.verify(socket.getSession().getPeerHost(), socket.getSession())) {
        throw new CertificateException("Hostname does not match!");
    }
    

    我看到HttpsURLConnection.getDefaultHostnameVerifier()可以返回一个默认的HostnameVerifier,我可以使用它来进行验证吗?我看到很多人都在谈论使用自定义的HostnameVerifier。我不理解如果已经有默认的验证器,为什么我们还需要自定义一个?

英文:

From this link, a demo for SSLSocketClient.java is given:

import java.net.*;
import java.io.*;
import javax.net.ssl.*;

/*
 * This example demostrates how to use a SSLSocket as client to
 * send a HTTP request and get response from an HTTPS server.
 * It assumes that the client is not behind a firewall
 */

public class SSLSocketClient {

    public static void main(String[] args) throws Exception {
        try {
            SSLSocketFactory factory =
                (SSLSocketFactory)SSLSocketFactory.getDefault();
            SSLSocket socket =
                (SSLSocket)factory.createSocket("www.verisign.com", 443);

            /*
             * send http request
             *
             * Before any application data is sent or received, the
             * SSL socket will do SSL handshaking first to set up
             * the security attributes.
             *
             * SSL handshaking can be initiated by either flushing data
             * down the pipe, or by starting the handshaking by hand.
             *
             * Handshaking is started manually in this example because
             * PrintWriter catches all IOExceptions (including
             * SSLExceptions), sets an internal error flag, and then
             * returns without rethrowing the exception.
             *
             * Unfortunately, this means any error messages are lost,
             * which caused lots of confusion for others using this
             * code.  The only way to tell there was an error is to call
             * PrintWriter.checkError().
             */
            socket.startHandshake();

            PrintWriter out = new PrintWriter(
                                  new BufferedWriter(
                                  new OutputStreamWriter(
                                  socket.getOutputStream())));

            out.println("GET / HTTP/1.0");
            out.println();
            out.flush();

            /*
             * Make sure there were no surprises
             */
            if (out.checkError())
                System.out.println(
                    "SSLSocketClient:  java.io.PrintWriter error");

            /* read response */
            BufferedReader in = new BufferedReader(
                                    new InputStreamReader(
                                    socket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null)
                System.out.println(inputLine);

            in.close();
            out.close();
            socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

I have two questions:

  1. According to this official document, if we are using a raw SSLSocketFactory rather than the HttpsURLConnection, there is no hostname verification enforced in the handshake process. Therefore, hostname verification should be done manually.
    > When using raw SSLSocket and SSLEngine classes, you should always check the peer's credentials before sending any data. The SSLSocket and SSLEngine classes do not automatically verify that the host name in a URL matches the host name in the peer's credentials. An application could be exploited with URL spoofing if the host name is not verified. Since JDK 7, endpoint identification/verification procedures can be handled during SSL/TLS handshaking. See the SSLParameters.getEndpointIdentificationAlgorithm method.

Does it mean the demo is insecure?

  1. I saw a solution to add hostname verification in Java 7 as:

    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    sslSocket.setSSLParameters(sslParams);

When the algorithm is specified as "HTTPS", the handshake will verify the hostname. Otherwise (the algorithm is empty only using raw SSLSockeFactory), the hostname verification has not been invoked at all.
I curious about could I fix it as follows:

SSLSocketFactory factory =
                    (SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket socket =
                    (SSLSocket)factory.createSocket("www.verisign.com", 443);
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
if(!hv.verify(socket.getSession().getPeerHost(),socket.getSession())){
    threw CertificateException("Hostname does not match!")
}

I saw the HttpsURLConnection.getDefaultHostnameVerifier() can return a default HostnameVerifier, can I use it to do verification? I saw many people talking about use a custom HostnameVerifier. I don't understand if there is a default one why we need to customize it?

答案1

得分: 1

(1) 是的,对于 HTTPS(正如您在引用后的段落中所提到的),这是一个安全漏洞;很可能这个示例是在 Java 7 之前编写的,并且自那以后没有更新。您可以提交一个 bug 报告,让他们进行更新。(当然,还有一些使用 SSL/TLS 应用程序,它们不验证主机名,比如 SNMPS 和 LDAPS,甚至没有 URL,但仍然可以使用 Java JSSE 实现。)

(2) HTTP 也是错误或不合格的:

  • PrintWriter 使用 JVM 的行分隔符,这根据平台而异,但 HTTP 标准(RFC 2068、2616、7230)要求在所有平台上请求头使用 CRLF,尽管一些服务器(可能包括 Google 在内)将接受传统的 Postel 原则“在发送时保守,在接收时开放”的只有 LF;

  • 读取部分假定所有数据都是基于行的,并且不会因规范化 EOL 而损坏,这对于 HTTP 头部和一些主体(比如大多数 Web 服务器在没有 Accept(或 Accept-encoding)的情况下收到的 text/html)是正确的,但不能保证;

  • 读取部分还假定所有数据可以从 JVM 默认的 'charset' 解码并重新编码安全地;这对于 HTTP 头部(实际上是 7 位 ASCII)是正确的,但对于许多主体来说并不是:特别是将 8859 或类似的编码处理为 UTF8 会破坏大部分内容,并且将 UTF8 处理为 8859 或 CP1252 会造成乱码。

(3) HTTP/1.0 已经正式过时,尽管它仍然得到广泛支持,并且制作一个明显更简单的演示,所以我会让这个问题暂且不管。

英文:

Borderline as an answer but got much too long for comments.

(1) yes, for HTTPS (as noted in the paragraph after the one you quoted) this is a security flaw; probably this example was written before Java 7 and not updated since. You could file a bug report for them to update it. (Of course there are some using SSL/TLS applications that don't validate hostname, like SNMPS and LDAPS, and don't even have URLs, but can still be implemented using Java JSSE.)

(2) the HTTP is wrong or poor also:

  • PrintWriter uses the JVM's lineSeparator which varies by platform, but HTTP standards (RFCs 2068, 2616, 7230) require CRLF for request header(s) on all platforms, though some servers (probably including google) will accept just-LF following the traditional Postel maxim 'be conservative in what you send and liberal in what you receive';

  • the read side assumes all data is line-oriented and won't be damaged by canonicalizing EOLs, which is true for HTTP header and some bodies like the text/html you will get from most webservers when request has no Accept (or Accept-encoding), but is not guaranteed;

  • the read side also assumes all data can be decoded from and re-encoded to the JVM default 'charset' safely; this is true for HTTP header (which is effectively 7-bit ASCII) but not many/most bodies: in particular handling 8859 or similar as UTF8 will destroy much of it, and handling UTF8 as 8859 or CP1252 will mojibake it.

(3) HTTP/1.0 is officially obsolete, although it is still widely supported and makes a significantly simpler demo, so I'd let that one slide.

huangapple
  • 本文由 发表于 2020年9月9日 11:29:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/63804352.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定