HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(一)

时间:2022-05-08
本文章向大家介绍HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(二)

HTTP strict transport security (HSTS) is defined in http://tools.ietf.org/html/ietf-websec-strict-transport-sec

HTTP-based dynamic public key pinning (HPKP) is defined in http://tools.ietf.org/html/ietf-websec-key-pinning.

深入理解需参考chromuim设计文档:

https://www.chromium.org/developers/design-documents

(中文版)https://ahangchen.gitbooks.io/chromium_doc_zh/content/zh//General_Architecture/Threading.html

chromuim在线源码:https://chromium.googlesource.com/chromium/src/+/58.0.3025.2/

------------------------------------------------分割线---------------------------------------------------------------------------------

通过观察文件名,发现涉及的文件主要是net目录下的:

Transport_security_persister.cc

Transport_security_persister.h

Transport_security_state.cc

Transport_security_state.h

Url_request_http_job.cc

在开始之前,需要从源码层面了解chromuim的网络栈部分,可参考《WebKit技术内幕》作者朱永盛的博文http://blog.csdn.net/milado_nju/article/details/9255563以及官网网络栈部分的文档http://www.chromium.org/developers/design-documents/network-stack。简单来说,发送网络请求基本都是通过URLRequest类,再根据不同协议选择不同的工厂,如HTTP为URLRequestHttpJob,由于HSTS针对HTTP,所以也只需关注URLRequestHttpJob;另一个重要的类是URLRequestContext,它包含其他完成URL请求的上下文信息,如cookie、主机解析、缓存以及HSTS信息等;许多URLRequest对象共享一个URLRequestContext。本文采用好理解的“自上而下”顺序来进行总结分析,但是实际中由于作者对chromuim完全没有认识,其实是从源码中搜索关键词看注释再查找引用一步一步摸索的。

 类URLRequestHttpJob的Factory方法在创建实例前,调用了Url_request_http_job.cc中的MaybeInternallyRedirect()函数对是否要进行升级HSTS进行了判断,通过名字可以猜出chromuim是采用内部重定向的方式来实现HTTP到HTTPS升级的,这跟使用开发者工具调试时观察到的请求一致。

MaybeInternallyRedirect函数中,根据request->url()来判断该域名是否应该升级HSTS,若hsts->ShouldUpgradeToSSL返回false,就返回nullptr,不用重定向;否则,使用Replacements类,根据request->url()的协议,来进行替换为https或wss(即websocket),再返回307状态码的URLRequestRedirectJob类。因此,URLRequestHttpJob返回URLRequestRedirectJob而非正常的URLRequestHttpJob,进行重定向来升级协议。由此可见,hsts的ShouldUpgradeToSSL方法就至关重要了,是否升级https全看它的返回值了。

 hsts是从request->context()->transport_security_state()取出,通过查看源码,即是URLRequest中的URLRequestContext指针类型变量context_中的TransportSecurityState*类型的transport_security_state变量。这句话有点绕,其实就是从URLRequestContext取出了保存HSTS信息的变量,该变量(260行)类型为TransportSecurityState*,TransportSecurityState就是实现HSTS机制的重点了。另外,源码指针的实现也呼应了开始提到的多URLRequest对一个URLRequestContext。

 Transport_security_state.h中可以看到TransportSecurityState的定义,它驻留在内存中追踪哪个域名启用了HSTS和PKP,并且用SetDelegate方法注册了一个代理来存储状态到硬盘中。为了解一个类,发现从其对应的单元测试文件可以得到一个整体的感性认识,单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。从单元测试文件Transport_security_state_unittest.cc中可以看出该类的简单用法,以及作者已经考虑到的一些避免HSTS被绕过的情况。比如这里就考虑了域名末尾加“.”和不加“.”在本策略中是一样的。并且,除了subdomain选项和preload外, 还有一些极端情况,如下所述。(Google C++ Testing Framework可参考这个网址http://developer.51cto.com/art/201108/285290.htm)

(1)防止拒绝服务的产生(比如添加一个域名为“.”的情况),

(2)对大小写不敏感(Google.com与GooGLe.CoM是一样的),

(3)规则冲突覆盖以更具体的为准(example.com的subdomain为true,而foo.example.com的subdomain为false,那么sub.foo.example.com的HSTS情况应为false;这点浏览器的实现情况应引起网站管理员的注意,可测试网站有没有这样的漏洞,RFC标准里我不记得提到),

(4)若设置了subdomain,不规范子域名也应使用HSTS(如2x01.foo.example.test返回true),

(5)域名删除的情况。

下面的这个810行的测试没看懂,为何exampl1.com一会儿期望为true,一会儿为false?猜想可能是在测试单元测试功能有没有正常工作吧。

 --------------------------------------------------分割线-------------------------------------------------

由单元测试和之前的源码分析以及源码中的注释可以得到,TransportSecurityState的ShouldUpgradeToSSL是判断该URL是否应该升级HTTPS的方法。查看ShouldUpgradeToSSL方法,发现是根据STSState类型变量的ShouldUpgradeToSSL()来返回布尔值的,从名称看依次检查了动态HSTS和预置的HSTS(即preload,如www.google.com已经预置在浏览器里了)。

先看类STSState,他是一个内部类,实现非常简单,注释说明它描述了HSTS的状态,属性包括过期时间、include_subdomains标识、域名等,根据host来由GetDynamicSTSState和GetStaticDomainState更新。首先来看GetDynamicSTSState,该函数根据host查询保存的信息,来对result赋值,并且特别强调了以更具体的结果为准;通过源码,其进行了host的规范化,然后用迭代器和enabled_sts_hosts_以及hash后的host(其实本地存储的HSTS信息文件中域名只有哈希后的值,为了隐私)来进行查询,并判断了查询结果中当前时间是否超过了过期时间,对结果result进行赋值为j->second,这样STSState就有值可以判断ShouldUpgradeToSSL了。ShouldUpgradeToSSL方法仅仅是比较upgrade_mode属性是否为MODE_FORCE_HTTPS。由此可见,STSStateMap类型的enabled_sts_hosts_就是本地维护HSTS信息的变量了,由此将我们引向了STSStateMap类。其实,若要发现实现细节方面的漏洞就需要详细看实现查找的算法了(CanonicalizeHost),对发现逻辑漏洞帮助不大,没有耐心的我就先略过算法细节分析。

bool TransportSecurityState::GetDynamicSTSState(const std::string& host,
                                                STSState* result) {
  DCHECK(CalledOnValidThread());

  const std::string canonicalized_host = CanonicalizeHost(host);
  if (canonicalized_host.empty())
    return false;

  base::Time current_time(base::Time::Now());

  for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) {
    std::string host_sub_chunk(&canonicalized_host[i],
                               canonicalized_host.size() - i);
    STSStateMap::iterator j = enabled_sts_hosts_.find(HashHost(host_sub_chunk));
    if (j == enabled_sts_hosts_.end())
      continue;

    // If the entry is invalid, drop it.
    if (current_time > j->second.expiry) {
      enabled_sts_hosts_.erase(j);
      DirtyNotify();
      continue;
    }

    // If this is the most specific STS match, add it to the result. Note: a STS
    // entry at a more specific domain overrides a less specific domain whether
    // or not |include_subdomains| is set.
    if (current_time <= j->second.expiry) {
      if (i == 0 || j->second.include_subdomains) {
        *result = j->second;
        result->domain = DNSDomainToString(host_sub_chunk);
        return true;
      }

      break;
    }
  }

  return false;
}

STSStateMap并不神秘,其实就是map<std::string, STSState>。那么具体是哪些函数对这个信息进行维护呢,通过对enabled_sts_hosts_查找引用,共12处,分布在EnableSTSHost、DeleteDynamicDataForHost、ClearDynamicData、DeleteAllDynamicDataSince、AddOrUpdateEnabledSTSHosts。

 EnableSTSHost首先对host进行规范化,然后根据state.ShouldUpgradeToSSL()是否应该升级https来决定是存入信息还是删除信息,存入的时候可以看到,保存的不是host,而是哈希后的host;该方法中已经使用了ShouldUpgradeToSSL,可见state内已经有信息,这一步只是进行存储的操作,还需要查看引用是谁调用EnableSTSHost传入了state。通过查找引用,发现是AddHSTSInternal方法,决定ShouldUpgradeToSSL返回值的upgrade_mode来自于该方法的参数,还需要继续寻找调用者。(感觉这俩方法合并效率会更高些)AddHSTSHeader根据解析HSTS头的ParseHSTSHeader函数返回的max_age变量InSeconds()==0来判断是否应该强制升级HTTPS,这种时间等于0的做法容易产生问题,负数、溢出是否影响?判断有无这种漏洞就再次需要详细看实现算法细节了。

 URLRequestHttpJob::ProcessStrictTransportSecurityHeader调用了AddHSTSHeader,实现中可以看到,浏览器仅接受HTTPS且没有证书错误的HSTS,从而避免恶意服务器拒绝服务其他域名;域名是IP地址也不会接受HSTS;有多个HSTS头则只接受第一个。注意到传入AddHSTSHeader作为host参数的是request_info_.url.host(),而这些变量不是通过参数传入而是类自身的变量,所以没必要再继续向上追。(当然缺乏经验的我肯定会犯错,向上追了好几步。ProcessStrictTransportSecurityHeader()被URLRequestHttpJob::NotifyHeadersComplete()调用,而调用NotifyHeadersComplete()的地方查找调用看似很多,好像处理头部时都会调用,只要注意限制在URLRequestHttpJob类,就发现其实只有一个调用者,SaveCookiesAndNotifyHeadersComplete,再网上追查两步就到了URLRequestHttpJob类的回调机制。但由于是检查HSTS头部时检查的ssl证书等信息,并且不存在其他路径到添加添加HSTS信息的函数,所以攻击者也难以通过别的通道来拒绝服务其他域名;本文只关心HSTS带来的问题,故没有必要再向上追寻,要寻找拒绝服务攻击可能性,关键就在于传入AddHSTSHeader的值了)

如果这个request_info_.url变量和ssl_info变量出问题:规范化后域名canonicalized_host哈希值碰撞(采用的SHA256几乎不可破)、和当前域名对应错误(若这个有问题那么cookie什么的就可以全乱套了,感觉出问题几率应该不大)、规范化后的域名和其他域名能一致(好多数据结构要看);则攻击者就可以操作其他域名HSTS信息。URLRequestHttpJob中request_info_.url是URLRequestHttpJob::Start()中赋值request_info_.url = request_->url();request_继承自父类URLRequestJob,指向了创建这个job的URLRequest。

到此为止,我们对HSTS实现的基本流程有了一定了解,但这仅仅是对管理enabled_sts_hosts_的EnableSTSHost函数追踪所学习到的,还有DeleteDynamicDataForHost、ClearDynamicData、DeleteAllDynamicDataSince、AddOrUpdateEnabledSTSHosts还有待继续学习。最后总结出如下两个图: