android HTTPS未校验服务器证书漏洞修复

最近项目被网络安全局扫出有网络安全问题,提示我们的app有未校验服务器证书漏洞,检测的步骤:在手机上安装抓包工具证书,就可以明文查看我们的app请求接口的报文,刚开始很不解,你都允许授权安装不可信任的证书到你的手机上了,看到你的明文请求报文不是很正常吗?这不是没事找事吗?经过检查发现项目中的代码确实也有不规范的地方,于是就这个机会做出修复动作,鉴于这个问题很难收到,且搜索到的也是一些没用的信息就发个贴记录一下,希望也能帮到遇到同样问题的伙伴。

我们网络不规范的地方就是重写了证书校验逻辑,绕过了证书校验和服务器合法性校验的逻辑。不规范的代码如下:

private static final X509TrustManager DEFAULT_TRUST_MANAGERS = new X509TrustManager() {

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
            // Trust.
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
            // Trust.
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };
 public TLSSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[] {DEFAULT_TRUST_MANAGERS}, new SecureRandom());
            delegate = sslContext.getSocketFactory();
        } catch (GeneralSecurityException e) {
            throw new AssertionError(); // The system has no TLS. Just give up.
        }
    }

上述代码就是重写了证书校验的逻辑,信任所有的证书,正确的逻辑是要校验证书是否是可信任的证书签发机构所认证的,如果不校验的话你虽然是用了https加密传输,只要你的客户端用户信任了其他的证书哪怕是非法的证书,一样能够请求通过,而这个证书的服务器就可以解密你的报文,拿到你的隐私信息了。

另外一个不合规的地方就是重写了域名合法性的校验,不规范的代码如下:

private static final HostnameVerifier HOSTNAME_VERIFIER = new HostnameVerifier() {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };

上述代码是重写了域名校验的逻辑,每个证书里携带的域名是不可更改的,如果证书是合法的证书签发机构签发的,并且域名是和你要请求的域名对的上的,基本上就能确认你访问就服务器地址就是要访问的服务器地址,这样才是安全的。如果你不校验上面两处的信息,别人就有机会拦截你的请求做一些非法事情,所以我们需要修复上面的错误逻辑。

正确的修复办法也很简单,我封装了工具类设置到网络框架的配置里即可。代码如下:

public class SSLSocketUtils {

    public static SSLSocketFactory newSslSocketFactory() {
        return newSslSocketFactory(platformTrustManager());
    }
    public static X509TrustManager platformTrustManager() {
        try {
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init((KeyStore) null);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:"
                        + Arrays.toString(trustManagers));
            }
            return (X509TrustManager) trustManagers[0];
        } catch (GeneralSecurityException e) {
            throw assertionError("No System TLS", e); // The system has no TLS. Just give up.
        }
    }

    private static SSLSocketFactory newSslSocketFactory(X509TrustManager trustManager) {
        try {
            SSLContext sslContext = Platform.get().getSSLContext();
            sslContext.init(null, new TrustManager[] { trustManager }, null);
            return sslContext.getSocketFactory();
        } catch (GeneralSecurityException e) {
            throw assertionError("No System TLS", e); // The system has no TLS. Just give up.
        }
    }
public final class OkHostnameVerifier implements HostnameVerifier {
  public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();

  private static final int ALT_DNS_NAME = 2;
  private static final int ALT_IPA_NAME = 7;

  private OkHostnameVerifier() {
  }

  @Override
  public boolean verify(String host, SSLSession session) {
    try {
      Certificate[] certificates = session.getPeerCertificates();
      return verify(host, (X509Certificate) certificates[0]);
    } catch (SSLException e) {
      return false;
    }
  }

  public boolean verify(String host, X509Certificate certificate) {
    return verifyAsIpAddress(host)
        ? verifyIpAddress(host, certificate)
        : verifyHostname(host, certificate);
  }

  /** Returns true if {@code certificate} matches {@code ipAddress}. */
  private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
    List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
    for (int i = 0, size = altNames.size(); i < size; i++) {
      if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
        return true;
      }
    }
    return false;
  }

  /** Returns true if {@code certificate} matches {@code hostname}. */
  private boolean verifyHostname(String hostname, X509Certificate certificate) {
    hostname = hostname.toLowerCase(Locale.US);
    List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
    for (String altName : altNames) {
      if (verifyHostname(hostname, altName)) {
        return true;
      }
    }
    return false;
  }

  public static List<String> allSubjectAltNames(X509Certificate certificate) {
    List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
    List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
    List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
    result.addAll(altIpaNames);
    result.addAll(altDnsNames);
    return result;
  }

  private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
    List<String> result = new ArrayList<>();
    try {
      Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
      if (subjectAltNames == null) {
        return Collections.emptyList();
      }
      for (Object subjectAltName : subjectAltNames) {
        List<?> entry = (List<?>) subjectAltName;
        if (entry == null || entry.size() < 2) {
          continue;
        }
        Integer altNameType = (Integer) entry.get(0);
        if (altNameType == null) {
          continue;
        }
        if (altNameType == type) {
          String altName = (String) entry.get(1);
          if (altName != null) {
            result.add(altName);
          }
        }
      }
      return result;
    } catch (CertificateParsingException e) {
      return Collections.emptyList();
    }
  }

  /**
   * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}.
   *
   * @param hostname lower-case host name.
   * @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code
   * *.android.com}.
   */
  public boolean verifyHostname(String hostname, String pattern) {
    // Basic sanity checks
    // Check length == 0 instead of .isEmpty() to support Java 5.
    if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith("."))
        || (hostname.endsWith(".."))) {
      // Invalid domain name
      return false;
    }
    if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith("."))
        || (pattern.endsWith(".."))) {
      // Invalid pattern/domain name
      return false;
    }

    // Normalize hostname and pattern by turning them into absolute domain names if they are not
    // yet absolute. This is needed because server certificates do not normally contain absolute
    // names or patterns, but they should be treated as absolute. At the same time, any hostname
    // presented to this method should also be treated as absolute for the purposes of matching
    // to the server certificate.
    //   www.android.com  matches www.android.com
    //   www.android.com  matches www.android.com.
    //   www.android.com. matches www.android.com.
    //   www.android.com. matches www.android.com
    if (!hostname.endsWith(".")) {
      hostname += '.';
    }
    if (!pattern.endsWith(".")) {
      pattern += '.';
    }
    // hostname and pattern are now absolute domain names.

    pattern = pattern.toLowerCase(Locale.US);
    // hostname and pattern are now in lower case -- domain names are case-insensitive.

    if (!pattern.contains("*")) {
      // Not a wildcard pattern -- hostname and pattern must match exactly.
      return hostname.equals(pattern);
    }
    // Wildcard pattern

    // WILDCARD PATTERN RULES:
    // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the
    //    only character in that label (i.e., must match the whole left-most label).
    //    For example, *.example.com is permitted, while *a.example.com, a*.example.com,
    //    a*b.example.com, a.*.example.com are not permitted.
    // 2. Asterisk (*) cannot match across domain name labels.
    //    For example, *.example.com matches test.example.com but does not match
    //    sub.test.example.com.
    // 3. Wildcard patterns for single-label domain names are not permitted.

    if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) {
      // Asterisk (*) is only permitted in the left-most domain name label and must be the only
      // character in that label
      return false;
    }

    // Optimization: check whether hostname is too short to match the pattern. hostName must be at
    // least as long as the pattern because asterisk must match the whole left-most label and
    // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters.
    if (hostname.length() < pattern.length()) {
      // hostname too short to match the pattern.
      return false;
    }

    if ("*.".equals(pattern)) {
      // Wildcard pattern for single-label domain name -- not permitted.
      return false;
    }

    // hostname must end with the region of pattern following the asterisk.
    String suffix = pattern.substring(1);
    if (!hostname.endsWith(suffix)) {
      // hostname does not end with the suffix
      return false;
    }

    // Check that asterisk did not match across domain name labels.
    int suffixStartIndexInHostname = hostname.length() - suffix.length();
    if ((suffixStartIndexInHostname > 0)
        && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) {
      // Asterisk is matching across domain name labels -- not permitted.
      return false;
    }

    // hostname matches pattern
    return true;
  }
}

OkHostnameVerifier 是直接拿的okhttp默认实现的,既安全有可靠有效。

然后把SSLSocketFactory 和OkHostnameVerifier设置到你的网络框架里就ok了。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
编写JavaScript代码实现用户登录时数据合法性校验功能,可以按照以下步骤进行: 1. 获取用户输入的用户名和密码。 2. 对用户名和密码进行非空校验,如果为空则提示用户输入。 3. 对用户名和密码进行长度校验,如果长度不符合要求则提示用户重新输入。 4. 对用户名和密码进行特殊字符校验,如果包含特殊字符则提示用户重新输入。 5. 如果用户名和密码均合法,则提交表单进行登录操作。 具体实现可以参考以下代码: ``` function validate() { var username = document.getElementById("username").value; var password = document.getElementById("password").value; // 非空校验 if (username == "" || password == "") { alert("用户名和密码不能为空!"); return false; } // 长度校验 if (username.length < 6 || username.length > 20 || password.length < 6 || password.length > 20) { alert("用户名和密码长度应在6-20个字符之间!"); return false; } // 特殊字符校验 var reg = /^[a-zA-Z-9_]+$/; if (!reg.test(username) || !reg.test(password)) { alert("用户名和密码只能包含字母、数字和下划线!"); return false; } // 提交表单 document.getElementById("loginForm").submit(); } ``` 在HTML代码中,可以将该函数绑定到登录按钮的点击事件上,例如: ``` <form id="loginForm" action="login.php" method="post"> <label>用户名:</label> <input type="text" id="username" name="username"><br> <label>密码:</label> <input type="password" id="password" name="password"><br> <input type="button" value="登录" onclick="validate()"> </form> ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值