Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhsts.cpp
Go to the documentation of this file.
1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qhsts_p.h"
6
7#include "qhttpheaders.h"
8
9#include "QtCore/private/qipaddress_p.h"
10#include "QtCore/qlist.h"
11
12#if QT_CONFIG(settings)
13#include "qhstsstore_p.h"
14#endif // QT_CONFIG(settings)
15
16QT_BEGIN_NAMESPACE
17
18static bool is_valid_domain_name(const QString &host)
19{
20 if (!host.size())
21 return false;
22
23 // RFC6797 8.1.1
24 // If the substring matching the host production from the Request-URI
25 // (of the message to which the host responded) syntactically matches
26 //the IP-literal or IPv4address productions from Section 3.2.2 of
27 //[RFC3986], then the UA MUST NOT note this host as a Known HSTS Host.
28 using namespace QIPAddressUtils;
29
30 IPv4Address ipv4Addr = {};
31 if (parseIp4(ipv4Addr, host.constBegin(), host.constEnd()))
32 return false;
33
34 IPv6Address ipv6Addr = {};
35 // Unlike parseIp4, parseIp6 returns nullptr if it managed to parse IPv6
36 // address successfully.
37 if (!parseIp6(ipv6Addr, host.constBegin(), host.constEnd()))
38 return false;
39
40 // TODO: for now we do not test IPvFuture address, it must be addressed
41 // by introducing parseIpFuture (actually, there is an implementation
42 // in QUrl that can be adopted/modified/moved to QIPAddressUtils).
43 return true;
44}
45
46void QHstsCache::updateFromHeaders(const QHttpHeaders &headers,
47 const QUrl &url)
48{
49 if (!url.isValid())
50 return;
51
52 QHstsHeaderParser parser;
53 if (parser.parse(headers)) {
54 updateKnownHost(url.host(), parser.expirationDate(), parser.includeSubDomains());
55#if QT_CONFIG(settings)
56 if (hstsStore)
57 hstsStore->synchronize();
58#endif // QT_CONFIG(settings)
59 }
60}
61
62void QHstsCache::updateFromPolicies(const QList<QHstsPolicy> &policies)
63{
64 for (const auto &policy : policies)
65 updateKnownHost(policy.host(), policy.expiry(), policy.includesSubDomains());
66
67#if QT_CONFIG(settings)
68 if (hstsStore && policies.size()) {
69 // These policies are coming either from store or from QNAM's setter
70 // function. As a result we can notice expired or new policies, time
71 // to sync ...
72 hstsStore->synchronize();
73 }
74#endif // QT_CONFIG(settings)
75}
76
77void QHstsCache::updateKnownHost(const QUrl &url, const QDateTime &expires,
78 bool includeSubDomains)
79{
80 if (!url.isValid())
81 return;
82
83 updateKnownHost(url.host(), expires, includeSubDomains);
84#if QT_CONFIG(settings)
85 if (hstsStore)
86 hstsStore->synchronize();
87#endif // QT_CONFIG(settings)
88}
89
90void QHstsCache::updateKnownHost(const QString &host, const QDateTime &expires,
91 bool includeSubDomains)
92{
93 if (!is_valid_domain_name(host))
94 return;
95
96 // HSTS is a per-host policy, regardless of protocol, port or any of the other
97 // details in an URL; so we only want the host part. QUrl::host handles
98 // IDNA 2003 (RFC3490) for us, as required by HSTS (RFC6797, section 10).
99 const HostName hostName(host);
100 const auto pos = knownHosts.find(hostName);
101 QHstsPolicy::PolicyFlags flags;
102 if (includeSubDomains)
103 flags = QHstsPolicy::IncludeSubDomains;
104
105 const QHstsPolicy newPolicy(expires, flags, hostName.name);
106 if (pos == knownHosts.end()) {
107 // A new, previously unknown host.
108 if (newPolicy.isExpired()) {
109 // Nothing to do at all - we did not know this host previously,
110 // we do not have to - since its policy expired.
111 return;
112 }
113
114 knownHosts.insert({hostName, newPolicy});
115#if QT_CONFIG(settings)
116 if (hstsStore)
117 hstsStore->addToObserved(newPolicy);
118#endif // QT_CONFIG(settings)
119 return;
120 }
121
122 if (newPolicy.isExpired())
123 knownHosts.erase(pos);
124 else if (pos->second != newPolicy)
125 pos->second = newPolicy;
126 else
127 return;
128
129#if QT_CONFIG(settings)
130 if (hstsStore)
131 hstsStore->addToObserved(newPolicy);
132#endif // QT_CONFIG(settings)
133}
134
135bool QHstsCache::isKnownHost(const QUrl &url) const
136{
137 if (!url.isValid() || !is_valid_domain_name(url.host()))
138 return false;
139
140 /*
141 RFC6797, 8.2. Known HSTS Host Domain Name Matching
142
143 * Superdomain Match
144 If a label-for-label match between an entire Known HSTS Host's
145 domain name and a right-hand portion of the given domain name
146 is found, then this Known HSTS Host's domain name is a
147 superdomain match for the given domain name. There could be
148 multiple superdomain matches for a given domain name.
149 * Congruent Match
150 If a label-for-label match between a Known HSTS Host's domain
151 name and the given domain name is found -- i.e., there are no
152 further labels to compare -- then the given domain name
153 congruently matches this Known HSTS Host.
154
155 We start from the congruent match, and then chop labels and dots and
156 proceed with superdomain match. While RFC6797 recommends to start from
157 superdomain, the result is the same - some valid policy will make a host
158 known.
159 */
160
161 bool superDomainMatch = false;
162 const QString hostNameAsString(url.host());
163 HostName nameToTest(QStringView{hostNameAsString});
164 while (nameToTest.fragment.size()) {
165 auto const pos = knownHosts.find(nameToTest);
166 if (pos != knownHosts.end()) {
167 if (pos->second.isExpired()) {
168 knownHosts.erase(pos);
169#if QT_CONFIG(settings)
170 if (hstsStore) {
171 // Inform our store that this policy has expired.
172 hstsStore->addToObserved(pos->second);
173 }
174#endif // QT_CONFIG(settings)
175 } else if (!superDomainMatch || pos->second.includesSubDomains()) {
176 return true;
177 }
178 }
179
180 const qsizetype dot = nameToTest.fragment.indexOf(u'.');
181 if (dot == -1)
182 break;
183
184 nameToTest.fragment = nameToTest.fragment.mid(dot + 1);
185 superDomainMatch = true;
186 }
187
188 return false;
189}
190
191void QHstsCache::clear()
192{
193 knownHosts.clear();
194}
195
196QList<QHstsPolicy> QHstsCache::policies() const
197{
198 QList<QHstsPolicy> values;
199 values.reserve(int(knownHosts.size()));
200 for (const auto &host : knownHosts)
201 values << host.second;
202 return values;
203}
204
205#if QT_CONFIG(settings)
206void QHstsCache::setStore(QHstsStore *store)
207{
208 // Caller retains ownership of store, which must outlive this cache.
209 if (store != hstsStore) {
210 hstsStore = store;
211
212 if (!hstsStore)
213 return;
214
215 // First we augment our store with the policies we already know about
216 // (and thus the cached policy takes priority over whatever policy we
217 // had in the store for the same host, if any).
218 if (knownHosts.size()) {
219 const QList<QHstsPolicy> observed(policies());
220 for (const auto &policy : observed)
221 hstsStore->addToObserved(policy);
222 hstsStore->synchronize();
223 }
224
225 // Now we update the cache with anything we have not observed yet, but
226 // the store knows about (well, it can happen we synchronize again as a
227 // result if some policies managed to expire or if we add a new one
228 // from the store to cache):
229 const QList<QHstsPolicy> restored(store->readPolicies());
230 updateFromPolicies(restored);
231 }
232}
233#endif // QT_CONFIG(settings)
234
235// The parser is quite simple: 'nextToken' knowns exactly what kind of tokens
236// are valid and it will return false if something else was found; then
237// we immediately stop parsing. 'parseDirective' knows how these tokens can
238// be combined into a valid directive and if some weird combination of
239// valid tokens is found - we immediately stop.
240// And finally we call parseDirective again and again until some error found or
241// we have no more bytes in the header.
242
243// The following isXXX functions are based on RFC2616, 2.2 Basic Rules.
244
245static bool isCHAR(int c)
246{
247 // CHAR = <any US-ASCII character (octets 0 - 127)>
248 return c >= 0 && c <= 127;
249}
250
251static bool isCTL(int c)
252{
253 // CTL = <any US-ASCII control character
254 // (octets 0 - 31) and DEL (127)>
255 return (c >= 0 && c <= 31) || c == 127;
256}
257
258
259static bool isLWS(int c)
260{
261 // LWS = [CRLF] 1*( SP | HT )
262 //
263 // CRLF = CR LF
264 // CR = <US-ASCII CR, carriage return (13)>
265 // LF = <US-ASCII LF, linefeed (10)>
266 // SP = <US-ASCII SP, space (32)>
267 // HT = <US-ASCII HT, horizontal-tab (9)>
268 //
269 // CRLF is handled by the time we parse a header (they were replaced with
270 // spaces). We only have to deal with remaining SP|HT
271 return c == ' ' || c == '\t';
272}
273
274static bool isTEXT(char c)
275{
276 // TEXT = <any OCTET except CTLs,
277 // but including LWS>
278 return !isCTL(c) || isLWS(c);
279}
280
281static bool isSeparator(char c)
282{
283 // separators = "(" | ")" | "<" | ">" | "@"
284 // | "," | ";" | ":" | "\" | <">
285 // | "/" | "[" | "]" | "?" | "="
286 // | "{" | "}" | SP | HT
287 static const char separators[] = "()<>@,;:\\\"/[]?={}";
288 static const char *end = separators + sizeof separators - 1;
289 return isLWS(c) || std::find(separators, end, c) != end;
290}
291
292static QByteArrayView unescapeMaxAge(QByteArrayView value)
293{
294 if (value.size() < 2 || value[0] != '"')
295 return value;
296
297 Q_ASSERT(value[value.size() - 1] == '"');
298 return value.mid(1, value.size() - 2);
299}
300
301static bool isTOKEN(char c)
302{
303 // token = 1*<any CHAR except CTLs or separators>
304 return isCHAR(c) && !isCTL(c) && !isSeparator(c);
305}
306
307/*
308
309RFC6797, 6.1 Strict-Transport-Security HTTP Response Header Field.
310Syntax:
311
312Strict-Tranposrt-Security = "Strict-Transport-Security" ":"
313 [ directive ] *( ";" [ directive ] )
314
315directive = directive-name [ "=" directive-value ]
316directive-name = token
317directive-value = token | quoted-string
318
319RFC 2616, 2.2 Basic Rules.
320
321token = 1*<any CHAR except CTLs or separators>
322quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
323
324
325qdtext = <any TEXT except <">>
326quoted-pair = "\" CHAR
327
328*/
329
330bool QHstsHeaderParser::parse(const QHttpHeaders &headers)
331{
332 for (const auto &value : headers.values(
333 QHttpHeaders::WellKnownHeader::StrictTransportSecurity)) {
334 header = value;
335 // RFC6797, 8.1:
336 //
337 // The UA MUST ignore any STS header fields not conforming to the
338 // grammar specified in Section 6.1 ("Strict-Transport-Security HTTP
339 // Response Header Field").
340 //
341 // If a UA receives more than one STS header field in an HTTP
342 // response message over secure transport, then the UA MUST process
343 // only the first such header field.
344 //
345 // We read this as: ignore all invalid headers and take the first valid:
346 if (parseSTSHeader() && maxAgeFound) {
347 expiry = QDateTime::currentDateTimeUtc().addSecs(maxAge);
348 return true;
349 }
350 }
351
352 // In case it was set by a syntactically correct header (but without
353 // REQUIRED max-age directive):
354 subDomainsFound = false;
355
356 return false;
357}
358
359bool QHstsHeaderParser::parseSTSHeader()
360{
361 expiry = QDateTime();
362 maxAgeFound = false;
363 subDomainsFound = false;
364 maxAge = 0;
365 tokenPos = 0;
366 token.clear();
367
368 while (tokenPos < header.size()) {
369 if (!parseDirective())
370 return false;
371
372 if (token.size() && token != ";") {
373 // After a directive we can only have a ";" or no more tokens.
374 // Invalid syntax.
375 return false;
376 }
377 }
378
379 return true;
380}
381
382bool QHstsHeaderParser::parseDirective()
383{
384 // RFC 6797, 6.1:
385 //
386 // directive = directive-name [ "=" directive-value ]
387 // directive-name = token
388 // directive-value = token | quoted-string
389
390
391 // RFC 2616, 2.2:
392 //
393 // token = 1*<any CHAR except CTLs or separators>
394
395 if (!nextToken())
396 return false;
397
398 if (!token.size()) // No more data, but no error.
399 return true;
400
401 if (token == ";") // That's a weird grammar, but that's what it is.
402 return true;
403
404 if (!isTOKEN(token.at(0))) // Not a valid directive-name.
405 return false;
406
407 const QByteArray directiveName = token;
408 // 2. Try to read "=" or ";".
409 if (!nextToken())
410 return false;
411
412 QByteArray directiveValue;
413 if (token == ";") // No directive-value
414 return processDirective(directiveName, directiveValue);
415
416 if (token == "=") {
417 // We expect a directive-value now:
418 if (!nextToken() || !token.size())
419 return false;
420 directiveValue = token;
421 } else if (token.size()) {
422 // Invalid syntax:
423 return false;
424 }
425
426 if (!processDirective(directiveName, directiveValue))
427 return false;
428
429 // Read either ";", or 'end of header', or some invalid token.
430 return nextToken();
431}
432
433bool QHstsHeaderParser::processDirective(const QByteArray &name, const QByteArray &value)
434{
435 Q_ASSERT(name.size());
436 // RFC6797 6.1/3 Directive names are case-insensitive
437 if (name.compare("max-age", Qt::CaseInsensitive) == 0) {
438 // RFC 6797, 6.1.1
439 // The syntax of the max-age directive's REQUIRED value (after
440 // quoted-string unescaping, if necessary) is defined as:
441 //
442 // max-age-value = delta-seconds
443 if (maxAgeFound) {
444 // RFC 6797, 6.1/2:
445 // All directives MUST appear only once in an STS header field.
446 return false;
447 }
448
449 const QByteArrayView unescapedValue = unescapeMaxAge(value);
450 if (!unescapedValue.size())
451 return false;
452
453 bool ok = false;
454 const qint64 age = unescapedValue.toLongLong(&ok);
455 if (!ok || age < 0)
456 return false;
457
458 maxAge = age;
459 maxAgeFound = true;
460 } else if (name.compare("includesubdomains", Qt::CaseInsensitive) == 0) {
461 // RFC 6797, 6.1.2. The includeSubDomains Directive.
462 // The OPTIONAL "includeSubDomains" directive is a valueless directive.
463
464 if (subDomainsFound) {
465 // RFC 6797, 6.1/2:
466 // All directives MUST appear only once in an STS header field.
467 return false;
468 }
469
470 subDomainsFound = true;
471 } // else we do nothing, skip unknown directives (RFC 6797, 6.1/5)
472
473 return true;
474}
475
476bool QHstsHeaderParser::nextToken()
477{
478 // Returns true if we found a valid token or we have no more data (token is
479 // empty then).
480
481 token.clear();
482
483 // Fortunately enough, by this point qhttpnetworkreply already got rid of
484 // [CRLF] parts, but we can have 1*(SP|HT) yet.
485 while (tokenPos < header.size() && isLWS(header.at(tokenPos)))
486 ++tokenPos;
487
488 if (tokenPos == header.size())
489 return true;
490
491 const char ch = header.at(tokenPos);
492 if (ch == ';' || ch == '=') {
493 token.append(ch);
494 ++tokenPos;
495 return true;
496 }
497
498 // RFC 2616, 2.2.
499 //
500 // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
501 // qdtext = <any TEXT except <">>
502 if (ch == '"') {
503 int last = tokenPos + 1;
504 while (last < header.size()) {
505 if (header.at(last) == '"') {
506 // The end of a quoted-string.
507 break;
508 } else if (header.at(last) == '\\') {
509 // quoted-pair = "\" CHAR
510 if (last + 1 < header.size() && isCHAR(header.at(last + 1)))
511 last += 2;
512 else
513 return false;
514 } else {
515 if (!isTEXT(header.at(last)))
516 return false;
517 ++last;
518 }
519 }
520
521 if (last >= header.size()) // no closing '"':
522 return false;
523
524 token = header.mid(tokenPos, last - tokenPos + 1);
525 tokenPos = last + 1;
526 return true;
527 }
528
529 // RFC 2616, 2.2:
530 //
531 // token = 1*<any CHAR except CTLs or separators>
532 if (!isTOKEN(ch))
533 return false;
534
535 int last = tokenPos + 1;
536 while (last < header.size() && isTOKEN(header.at(last)))
537 ++last;
538
539 token = header.mid(tokenPos, last - tokenPos);
540 tokenPos = last;
541
542 return true;
543}
544
545QT_END_NAMESPACE
static QByteArrayView unescapeMaxAge(QByteArrayView value)
Definition qhsts.cpp:292
static bool isCTL(int c)
Definition qhsts.cpp:251
static bool isLWS(int c)
Definition qhsts.cpp:259
static bool isCHAR(int c)
Definition qhsts.cpp:245
static bool isTEXT(char c)
Definition qhsts.cpp:274
static bool isTOKEN(char c)
Definition qhsts.cpp:301
static bool isSeparator(char c)
Definition qhsts.cpp:281