Security: HSTS host matching ignores superdomains with includeSubDomains when a subdomain without includeSubDomains is present |
||||||
Issue descriptionVERSION Chrome Version: 64.0.3282.140 stable (also ToT) Operating System: gLinux (but likely affects several OSes) REPRODUCTION CASE 1. Go to chrome://net-internals/#hsts 2. Add HSTS/PKP domain: "sub.example.com" and include subdomains for STS 3. Add HSTS/PKP domain: "sub.sub.example.com" without including subdomains for STS 4. Query HSTS/PKP domain: "x.sub.example.com" --> a record is found, working as intended 5. Query HSTS/PKP domain: "x.sub.sub.example.com" --> no record is found, this is the bug The same behavior is visible in navigations, i.e. http://x.sub.example.com/ redirects to https, whereas http://x.sub.sub.example.com/ doesn't. RFC6797 mandates a different behavior: "If, when performing domain name matching any superdomain match with an asserted includeSubDomains directive is found, or, if no superdomain matches with asserted includeSubDomains directives are found and a congruent match is found (with or without an asserted includeSubDomains directive), ..."
,
Mar 14 2018
I didn't think any browser implemented that behaviour. Indeed, we know it's important for a number of deployments to *not* match that behaviour, and to have the implementation behave as today, as that allows 'carveouts' That is, consider example.com, includeSubdomains corp.example.com, no includeSubDomains The behaviour allows one to 'punch out' corp.google.com from the subdomain list, while otherwise setting it on the superdomain. Without that ability, we know a number of organizations would not deploy HSTS, due to these split horizons. I'm not sure we should tag as a security bug, since this (is/was) intentional deviation.
,
Mar 14 2018
,
Mar 14 2018
Thanks Ryan! I understand the use case for the current implementation, but that may harm deployment that rely on setting recursive on their primary domain as a security measure. Is this deviation from the spec documented somewhere? Or maybe the spec should be updated?
,
Mar 14 2018
Our behavior is also pretty weird in that, while includeSubdomains behaves like this between two dynamic pin entries, a non-recursive corp.example.com HSTS header will not mask off a recursive static HSTS entry. I vaguely recall us considering this a bug when the organization which was relying on this came to us (and specifically asked that static entries have the same behavior, which we declined to do). We talked about adding the carve-out mechanism to admin policy instead so that we could ultimately align this with the spec. Or perhaps it was only me who thought we should go that route, I forget. :-) This kind of carve-out has security issues because of how cookies work (though we have the __Secure- prefix now), so I think admin policies are a better mechanism.
,
Mar 14 2018
> This kind of carve-out has security issues because of how cookies work I should probably have expanded on that: Servers cannot distinguish cookies set over HTTP and HTTPS, so you need includeSubDomains to plug that hole. Thus having a carve-out for corp.example.com at all is a bad idea, whether it be done via HTTP header or admin policy. It would be preferable if one did not need this at all. Alas, that's not how a lot of things are deployed, so admin policy would allow us to at least contain the effects to folks who would actually be visiting foo.corp.example.com. It also works better as it doesn't require one hit corp.example.com before visiting foo.corp.example.com. (If we felt extra-inclined, we could even tweak how cookies work based on that policy, but that may be a bit much.)
,
Mar 15 2018
> Our behavior is also pretty weird in that, while includeSubdomains behaves > like this between two dynamic pin entries, a non-recursive corp.example.com > HSTS header will not mask off a recursive static HSTS entry. Really? To me it looks the other way around: In my reading of TransportSecurityState::ShouldUpgradeToSSL() any dynamic entry takes precedence over any static entry. Therefore a dynamic non-recursive entry can punch a hole into a static recursive entry, but a static non-recursive entry cannot affect any dynamic entry. I have a hard time convincing myself that this is working as intended and I agree with David that carveouts via enterprise policy probably would be a better approach.
,
Mar 15 2018
> Therefore a dynamic non-recursive entry can punch a hole into a static recursive entry, but a static non-recursive entry cannot affect any dynamic entry. Yes, that was the desired behaviour I was speaking about. Staticly loading example.com, dynamically punching out corp.example.com
,
Mar 15 2018
,
Mar 15 2018
I don't believe that is actually the behavior. In TransportSecurityState::ShouldUpgradeToSSL, if there is a non-recursive dynamic entry, GetDynamicSTSState will not return any state (because nothing matches), which means it will fall through to checking static entries, where it will find a match and force an upgrade. (It's confusing because the code makes it look like STSState entries where ShouldUpgradeToSSL returns false exist. They don't in the dynamic map and only exist in the static map as a quirk of how the static map merges STS and PKP state together.)
,
Mar 15 2018
(This is precisely the behavior we were asked to change by the organization in comment #5 and declined to, in favor of admin policy that never materialized.)
,
Apr 23 2018
To summarize, there's two ways in which the implementation deviates from the RFC: 1. include subdomains does not take precedence: * example.com, includeSubdomains * corp.example.com, no includeSubDomains --> corp.example.com without includeSubDomains takes precedence 2. dynamic takes precedence over static: * sub.example.com, no includeSubdomains, static * example.com, includeSubdomains, dynamic --> example.com with includeSubdomains takes precedence For someone planning to roll out a complex HSTS deployment, it seems the only way of knowing how Chrome is going to behave is reading our source code. I don't think that's a good state to be in.
,
Apr 23 2018
Does (2) deviate from the RFC? I thought (2) was the RFC's behavior while (1) was a deviation. (Of course, (2) is interesting in so far as it is inconsistent with (1). Just wanted to make sure we're on the same page.) One could imaging aligning (2) with (1), but I gather you aren't a fan of that and neither am I. Given the use case Ryan mentioned, probably any option that aligns (1) with (2) will require first adding an admin policy source of punch-outs. Actually, how do other browsers behave here? Just about everyone implements HSTS at this point.
,
Apr 24 2018
In my reading of the RFC, I couldn't find any mention of static entries being treated differently than dynamic entries other than that they don't expire. Chrome's behavior of first searching dynamic and then static entries deviates from that. My example was incorrect, though; let me try again: 2. dynamic takes precedence over static: * example.com, includeSubdomains, static * example.com, no includeSubdomains, dynamic --> example.com without includeSubdomains takes precedence Regarding ways forward, I don't have enough context to have an opinion other than that Chrome and RFC should be brought into alignment (either by changing Chrome or by changing the RFC).
,
Apr 24 2018
Changing the RFC is not really an option, but nor should we treat the RFC behaviour as the desired correct behaviour. I think this is Type-Cleanup, doesn't need to RVG, SecSeverity-None, Pri-3
,
Apr 24 2018
> nor should we treat the RFC behaviour as the desired correct behaviour. I thought Chrome was aiming to be standards compliant?
,
Apr 24 2018
I think this is a conversation best not held on a bug, and ideally, we will derestrict this bug. Chrome's interest is in being the user's agent, and at times, that may require deviation from specifications when specifications work against those interests. For example, Chrome allows IP addresses to be treated on the HSTS list (when statically loaded), despite the specification clearly not allowing HSTS to be set for IP addresses. This does, however, help protect certain DNS resolvers. We take a pragmatic approach to standards, and if you can look at Chrome's involvement in SDOs, has preferred to work with living standards that align with reality rather than idealized standards that, intentional or not, have made poor choices.
,
Apr 25 2018
I'm fine with derestricting this bug but I still consider it a security issue because site operators may look to the RFC when configuring HSTS and Chrome's deviation from the RFC may cause HSTS to be off in cases where the site operator intended it to be on.
,
Apr 25 2018
I'll let the security-sheriff derestrict, but I also disagree that it's a security bug, in that it only applies to the case of static HSTS as well, which is not a strict security boundary (best effort).
,
Apr 25 2018
> I couldn't find any mention of static entries being treated differently than dynamic entries other than that they don't expire. The spec doesn't says anything about static lists at all. Just "Other mechanisms, such as a client-side pre-loaded Known HSTS Host list, MAY also be used" without details, and some non-normative text saying implementors "should consider including" a pre-load list or some sort. The details are left to the implementor. Indeed if you were to try to shoehorn the static list into the dynamic list's model, as a dynamic entry with infinite max-age, you would get undesirable behavior where an expiring dynamic entry would not cause the static entry to resurface. Newer HSTS headers with shorter max-age or no includeSubDomains still clobber older HSTS entries with longer ones or includeSubDomains. This is a necessarily practicality for sites that mess up. But this is all moot because the updated example is still off: > 2. dynamic takes precedence over static: > * example.com, includeSubdomains, static > * example.com, no includeSubdomains, dynamic > --> example.com without includeSubdomains takes precedence It doesn't matter that, when you query example.com, whether the static or dynamic includeSubDomains bit is surfaced because you are querying example.com. Were you to query foo.example.com instead, any entries disqualified by includeSubDomains would be filtered out at the matching stage.
,
May 3 2018
De-restricted.
,
May 3 2018
|
||||||
►
Sign in to add a comment |
||||||
Comment 1 by mkwst@chromium.org
, Mar 14 2018Labels: Security_Severity-Low Security_Impact-Stable
Status: Untriaged (was: Unconfirmed)