1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtTest/QtTest> |
30 | |
31 | #include <QtCore/qdatetime.h> |
32 | #include <QtCore/qvector.h> |
33 | #include <QtCore/qpair.h> |
34 | #include <QtCore/qurl.h> |
35 | #include <QtCore/qdir.h> |
36 | |
37 | #include <QtNetwork/private/qhstsstore_p.h> |
38 | #include <QtNetwork/private/qhsts_p.h> |
39 | |
40 | QT_USE_NAMESPACE |
41 | |
42 | class tst_QHsts : public QObject |
43 | { |
44 | Q_OBJECT |
45 | private Q_SLOTS: |
46 | void testSingleKnownHost_data(); |
47 | void testSingleKnownHost(); |
48 | void testMultilpeKnownHosts(); |
49 | void testPolicyExpiration(); |
50 | void testSTSHeaderParser(); |
51 | void testStore(); |
52 | }; |
53 | |
54 | void tst_QHsts::testSingleKnownHost_data() |
55 | { |
56 | QTest::addColumn<QUrl>(name: "knownHost" ); |
57 | QTest::addColumn<QDateTime>(name: "policyExpires" ); |
58 | QTest::addColumn<bool>(name: "includeSubDomains" ); |
59 | QTest::addColumn<QUrl>(name: "hostToTest" ); |
60 | QTest::addColumn<bool>(name: "isKnown" ); |
61 | |
62 | const QDateTime currentUTC = QDateTime::currentDateTimeUtc(); |
63 | const QUrl knownHost(QLatin1String("http://example.com" )); |
64 | const QUrl validSubdomain(QLatin1String("https://sub.example.com/ohoho" )); |
65 | const QUrl unknownDomain(QLatin1String("http://example.org" )); |
66 | const QUrl subSubdomain(QLatin1String("https://level3.level2.example.com" )); |
67 | |
68 | const QDateTime validDate(currentUTC.addSecs(secs: 1000)); |
69 | QTest::newRow(dataTag: "same-known" ) << knownHost << validDate << false << knownHost << true; |
70 | QTest::newRow(dataTag: "subexcluded" ) << knownHost << validDate << false << validSubdomain << false; |
71 | QTest::newRow(dataTag: "subincluded" ) << knownHost << validDate << true << validSubdomain << true; |
72 | QTest::newRow(dataTag: "unknown-subexcluded" ) << knownHost << validDate << false << unknownDomain << false; |
73 | QTest::newRow(dataTag: "unknown-subincluded" ) << knownHost << validDate << true << unknownDomain << false; |
74 | QTest::newRow(dataTag: "sub-subdomain-subincluded" ) << knownHost << validDate << true << subSubdomain << true; |
75 | QTest::newRow(dataTag: "sub-subdomain-subexcluded" ) << knownHost << validDate << false << subSubdomain << false; |
76 | |
77 | const QDateTime invalidDate; |
78 | QTest::newRow(dataTag: "invalid-time" ) << knownHost << invalidDate << false << knownHost << false; |
79 | QTest::newRow(dataTag: "invalid-time-subexcluded" ) << knownHost << invalidDate << false |
80 | << validSubdomain << false; |
81 | QTest::newRow(dataTag: "invalid-time-subincluded" ) << knownHost << invalidDate << true |
82 | << validSubdomain << false; |
83 | |
84 | const QDateTime expiredDate(currentUTC.addSecs(secs: -1000)); |
85 | QTest::newRow(dataTag: "expired-time" ) << knownHost << expiredDate << false << knownHost << false; |
86 | QTest::newRow(dataTag: "expired-time-subexcluded" ) << knownHost << expiredDate << false |
87 | << validSubdomain << false; |
88 | QTest::newRow(dataTag: "expired-time-subincluded" ) << knownHost << expiredDate << true |
89 | << validSubdomain << false; |
90 | const QUrl ipAsHost(QLatin1String("http://127.0.0.1" )); |
91 | QTest::newRow(dataTag: "ip-address-in-hostname" ) << ipAsHost << validDate << false |
92 | << ipAsHost << false; |
93 | |
94 | const QUrl anyIPv4AsHost(QLatin1String("http://0.0.0.0" )); |
95 | QTest::newRow(dataTag: "anyip4-address-in-hostname" ) << anyIPv4AsHost << validDate |
96 | << false << anyIPv4AsHost << false; |
97 | const QUrl anyIPv6AsHost(QLatin1String("http://[::]" )); |
98 | QTest::newRow(dataTag: "anyip6-address-in-hostname" ) << anyIPv6AsHost << validDate |
99 | << false << anyIPv6AsHost << false; |
100 | |
101 | } |
102 | |
103 | void tst_QHsts::testSingleKnownHost() |
104 | { |
105 | QFETCH(const QUrl, knownHost); |
106 | QFETCH(const QDateTime, policyExpires); |
107 | QFETCH(const bool, includeSubDomains); |
108 | QFETCH(const QUrl, hostToTest); |
109 | QFETCH(const bool, isKnown); |
110 | |
111 | QHstsCache cache; |
112 | cache.updateKnownHost(url: knownHost, expires: policyExpires, includeSubDomains); |
113 | QCOMPARE(cache.isKnownHost(hostToTest), isKnown); |
114 | } |
115 | |
116 | void tst_QHsts::testMultilpeKnownHosts() |
117 | { |
118 | const QDateTime currentUTC = QDateTime::currentDateTimeUtc(); |
119 | const QDateTime validDate(currentUTC.addSecs(secs: 10000)); |
120 | const QDateTime expiredDate(currentUTC.addSecs(secs: -10000)); |
121 | const QUrl exampleCom(QLatin1String("https://example.com" )); |
122 | const QUrl subExampleCom(QLatin1String("https://sub.example.com" )); |
123 | |
124 | QHstsCache cache; |
125 | // example.com is HSTS and includes subdomains: |
126 | cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true); |
127 | QVERIFY(cache.isKnownHost(exampleCom)); |
128 | QVERIFY(cache.isKnownHost(subExampleCom)); |
129 | // example.com can set its policy not to include subdomains: |
130 | cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: false); |
131 | QVERIFY(!cache.isKnownHost(subExampleCom)); |
132 | // but sub.example.com can set its own policy: |
133 | cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: false); |
134 | QVERIFY(cache.isKnownHost(subExampleCom)); |
135 | // let's say example.com's policy has expired: |
136 | cache.updateKnownHost(url: exampleCom, expires: expiredDate, includeSubDomains: false); |
137 | QVERIFY(!cache.isKnownHost(exampleCom)); |
138 | // it should not affect sub.example.com's policy: |
139 | QVERIFY(cache.isKnownHost(subExampleCom)); |
140 | |
141 | // clear cache and invalidate all policies: |
142 | cache.clear(); |
143 | QVERIFY(!cache.isKnownHost(exampleCom)); |
144 | QVERIFY(!cache.isKnownHost(subExampleCom)); |
145 | |
146 | // siblings: |
147 | const QUrl anotherSub(QLatin1String("https://sub2.example.com" )); |
148 | cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: true); |
149 | cache.updateKnownHost(url: anotherSub, expires: validDate, includeSubDomains: true); |
150 | QVERIFY(cache.isKnownHost(subExampleCom)); |
151 | QVERIFY(cache.isKnownHost(anotherSub)); |
152 | // they cannot set superdomain's policy: |
153 | QVERIFY(!cache.isKnownHost(exampleCom)); |
154 | // a sibling cannot set another sibling's policy: |
155 | cache.updateKnownHost(url: anotherSub, expires: expiredDate, includeSubDomains: false); |
156 | QVERIFY(cache.isKnownHost(subExampleCom)); |
157 | QVERIFY(!cache.isKnownHost(anotherSub)); |
158 | QVERIFY(!cache.isKnownHost(exampleCom)); |
159 | // let's make example.com known again: |
160 | cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true); |
161 | // a subdomain cannot affect its superdomain's policy: |
162 | cache.updateKnownHost(url: subExampleCom, expires: expiredDate, includeSubDomains: true); |
163 | QVERIFY(cache.isKnownHost(exampleCom)); |
164 | // and this superdomain includes subdomains in its HSTS policy: |
165 | QVERIFY(cache.isKnownHost(subExampleCom)); |
166 | QVERIFY(cache.isKnownHost(anotherSub)); |
167 | |
168 | // a subdomain (with its subdomains) cannot affect its superdomain's policy: |
169 | cache.updateKnownHost(url: exampleCom, expires: expiredDate, includeSubDomains: true); |
170 | cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: true); |
171 | QVERIFY(cache.isKnownHost(subExampleCom)); |
172 | QVERIFY(!cache.isKnownHost(exampleCom)); |
173 | } |
174 | |
175 | void tst_QHsts::testPolicyExpiration() |
176 | { |
177 | QDateTime currentUTC = QDateTime::currentDateTimeUtc(); |
178 | const QUrl exampleCom(QLatin1String("http://example.com" )); |
179 | const QUrl subdomain(QLatin1String("http://subdomain.example.com" )); |
180 | const qint64 lifeTimeMS = 50; |
181 | |
182 | QHstsCache cache; |
183 | // start with 'includeSubDomains' and 5 s. lifetime: |
184 | cache.updateKnownHost(url: exampleCom, expires: currentUTC.addMSecs(msecs: lifeTimeMS), includeSubDomains: true); |
185 | QVERIFY(cache.isKnownHost(exampleCom)); |
186 | QVERIFY(cache.isKnownHost(subdomain)); |
187 | // wait for approx. a half of lifetime: |
188 | QTest::qWait(ms: lifeTimeMS / 2); |
189 | |
190 | if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(msecs: lifeTimeMS)) { |
191 | // Should still be valid: |
192 | QVERIFY(cache.isKnownHost(exampleCom)); |
193 | QVERIFY(cache.isKnownHost(subdomain)); |
194 | } |
195 | |
196 | QTest::qWait(ms: lifeTimeMS); |
197 | // expired: |
198 | QVERIFY(!cache.isKnownHost(exampleCom)); |
199 | QVERIFY(!cache.isKnownHost(subdomain)); |
200 | |
201 | // now check that superdomain's policy expires, but not subdomain's policy: |
202 | currentUTC = QDateTime::currentDateTimeUtc(); |
203 | cache.updateKnownHost(url: exampleCom, expires: currentUTC.addMSecs(msecs: lifeTimeMS / 5), includeSubDomains: true); |
204 | cache.updateKnownHost(url: subdomain, expires: currentUTC.addMSecs(msecs: lifeTimeMS), includeSubDomains: true); |
205 | QVERIFY(cache.isKnownHost(exampleCom)); |
206 | QVERIFY(cache.isKnownHost(subdomain)); |
207 | QTest::qWait(ms: lifeTimeMS / 2); |
208 | if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(msecs: lifeTimeMS)) { |
209 | QVERIFY(!cache.isKnownHost(exampleCom)); |
210 | QVERIFY(cache.isKnownHost(subdomain)); |
211 | } |
212 | } |
213 | |
214 | void tst_QHsts::() |
215 | { |
216 | QHstsHeaderParser parser; |
217 | using = QPair<QByteArray, QByteArray>; |
218 | using = QList<Header>; |
219 | |
220 | QVERIFY(!parser.includeSubDomains()); |
221 | QVERIFY(!parser.expirationDate().isValid()); |
222 | Headers list; |
223 | QVERIFY(!parser.parse(list)); |
224 | QVERIFY(!parser.includeSubDomains()); |
225 | QVERIFY(!parser.expirationDate().isValid()); |
226 | |
227 | list << Header("Strict-Transport-security" , "200" ); |
228 | QVERIFY(!parser.parse(list)); |
229 | QVERIFY(!parser.includeSubDomains()); |
230 | QVERIFY(!parser.expirationDate().isValid()); |
231 | |
232 | // This header is missing REQUIRED max-age directive, so we'll ignore it: |
233 | list << Header("Strict-Transport-Security" , "includeSubDomains" ); |
234 | QVERIFY(!parser.parse(list)); |
235 | QVERIFY(!parser.includeSubDomains()); |
236 | QVERIFY(!parser.expirationDate().isValid()); |
237 | |
238 | list.pop_back(); |
239 | list << Header("Strict-Transport-Security" , "includeSubDomains;max-age=1000" ); |
240 | QVERIFY(parser.parse(list)); |
241 | QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc()); |
242 | QVERIFY(parser.includeSubDomains()); |
243 | |
244 | list.pop_back(); |
245 | // Invalid (includeSubDomains twice): |
246 | list << Header("Strict-Transport-Security" , "max-age = 1000 ; includeSubDomains;includeSubDomains" ); |
247 | QVERIFY(!parser.parse(list)); |
248 | QVERIFY(!parser.includeSubDomains()); |
249 | QVERIFY(!parser.expirationDate().isValid()); |
250 | |
251 | list.pop_back(); |
252 | // Invalid (weird number of seconds): |
253 | list << Header("Strict-Transport-Security" , "max-age=-1000 ; includeSubDomains" ); |
254 | QVERIFY(!parser.parse(list)); |
255 | QVERIFY(!parser.includeSubDomains()); |
256 | QVERIFY(!parser.expirationDate().isValid()); |
257 | |
258 | list.pop_back(); |
259 | // Note, directives are case-insensitive + we should ignore unknown directive. |
260 | list << Header("Strict-Transport-Security" , ";max-age=1000 ;includesubdomains;;" |
261 | "nowsomeunknownheader=\"somevaluewithescapes\\;\"" ); |
262 | QVERIFY(parser.parse(list)); |
263 | QVERIFY(parser.includeSubDomains()); |
264 | QVERIFY(parser.expirationDate().isValid()); |
265 | |
266 | list.pop_back(); |
267 | // Check that we know how to unescape max-age: |
268 | list << Header("Strict-Transport-Security" , "max-age=\"1000\"" ); |
269 | QVERIFY(parser.parse(list)); |
270 | QVERIFY(!parser.includeSubDomains()); |
271 | QVERIFY(parser.expirationDate().isValid()); |
272 | |
273 | list.pop_back(); |
274 | // The only STS header, with invalid syntax though, to be ignored: |
275 | list << Header("Strict-Transport-Security" , "max-age; max-age=15768000" ); |
276 | QVERIFY(!parser.parse(list)); |
277 | QVERIFY(!parser.includeSubDomains()); |
278 | QVERIFY(!parser.expirationDate().isValid()); |
279 | |
280 | // Now we check that our parse chosses the first valid STS header and ignores |
281 | // others: |
282 | list.clear(); |
283 | list << Header("Strict-Transport-Security" , "includeSubdomains; max-age=\"hehehe\";" ); |
284 | list << Header("Strict-Transport-Security" , "max-age=10101" ); |
285 | QVERIFY(parser.parse(list)); |
286 | QVERIFY(!parser.includeSubDomains()); |
287 | QVERIFY(parser.expirationDate().isValid()); |
288 | |
289 | |
290 | list.clear(); |
291 | list << Header("Strict-Transport-Security" , "max-age=0" ); |
292 | QVERIFY(parser.parse(list)); |
293 | QVERIFY(!parser.includeSubDomains()); |
294 | QVERIFY(parser.expirationDate() <= QDateTime::currentDateTimeUtc()); |
295 | |
296 | // Parsing is case-insensitive: |
297 | list.pop_back(); |
298 | list << Header("Strict-Transport-Security" , "Max-aGE=1000; InclUdesUbdomains" ); |
299 | QVERIFY(parser.parse(list)); |
300 | QVERIFY(parser.includeSubDomains()); |
301 | QVERIFY(parser.expirationDate().isValid()); |
302 | |
303 | // Grammar of STS header is quite permissive, let's check we can parse |
304 | // some weird but valid header: |
305 | list.pop_back(); |
306 | list << Header("Strict-Transport-Security" , ";;; max-age = 17; ; ; ; ;;; ;;" |
307 | ";;; ; includeSubdomains ;;thisIsUnknownDirective;;;;" ); |
308 | QVERIFY(parser.parse(list)); |
309 | QVERIFY(parser.includeSubDomains()); |
310 | QVERIFY(parser.expirationDate().isValid()); |
311 | |
312 | list.pop_back(); |
313 | list << Header("Strict-Transport-Security" , "max-age=1000; includeSubDomains bogon" ); |
314 | QVERIFY(!parser.parse(list)); |
315 | QVERIFY(!parser.includeSubDomains()); |
316 | QVERIFY(!parser.expirationDate().isValid()); |
317 | } |
318 | |
319 | const QLatin1String storeDir("." ); |
320 | |
321 | struct TestStoreDeleter |
322 | { |
323 | ~TestStoreDeleter() |
324 | { |
325 | QDir cwd; |
326 | if (!cwd.remove(fileName: QHstsStore::absoluteFilePath(dirName: storeDir))) |
327 | qWarning() << "tst_QHsts::testStore: failed to remove the hsts store file" ; |
328 | } |
329 | }; |
330 | |
331 | void tst_QHsts::testStore() |
332 | { |
333 | // Delete the store's file after we finish the test. |
334 | TestStoreDeleter cleaner; |
335 | |
336 | const QUrl exampleCom(QStringLiteral("http://example.com" )); |
337 | const QUrl subDomain(QStringLiteral("http://subdomain.example.com" )); |
338 | const QDateTime validDate(QDateTime::currentDateTimeUtc().addDays(days: 1)); |
339 | |
340 | { |
341 | // We start from an empty cache and empty store: |
342 | QHstsCache cache; |
343 | QHstsStore store(storeDir); |
344 | cache.setStore(&store); |
345 | QVERIFY(!cache.isKnownHost(exampleCom)); |
346 | QVERIFY(!cache.isKnownHost(subDomain)); |
347 | // (1) This will also store the policy: |
348 | cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true); |
349 | QVERIFY(cache.isKnownHost(exampleCom)); |
350 | QVERIFY(cache.isKnownHost(subDomain)); |
351 | } |
352 | { |
353 | // Test the policy stored at (1): |
354 | QHstsCache cache; |
355 | QHstsStore store(storeDir); |
356 | cache.setStore(&store); |
357 | QVERIFY(cache.isKnownHost(exampleCom)); |
358 | QVERIFY(cache.isKnownHost(subDomain)); |
359 | // (2) Remove subdomains: |
360 | cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: false); |
361 | QVERIFY(!cache.isKnownHost(subDomain)); |
362 | } |
363 | { |
364 | // Test the previous update (2): |
365 | QHstsCache cache; |
366 | QHstsStore store(storeDir); |
367 | cache.setStore(&store); |
368 | QVERIFY(cache.isKnownHost(exampleCom)); |
369 | QVERIFY(!cache.isKnownHost(subDomain)); |
370 | } |
371 | { |
372 | QHstsCache cache; |
373 | cache.updateKnownHost(url: subDomain, expires: validDate, includeSubDomains: false); |
374 | QVERIFY(cache.isKnownHost(subDomain)); |
375 | QHstsStore store(storeDir); |
376 | // (3) This should store policy from cache, over old policy from store: |
377 | cache.setStore(&store); |
378 | } |
379 | { |
380 | // Test that (3) was stored: |
381 | QHstsCache cache; |
382 | QHstsStore store(storeDir); |
383 | cache.setStore(&store); |
384 | QVERIFY(cache.isKnownHost(subDomain)); |
385 | } |
386 | } |
387 | |
388 | QTEST_MAIN(tst_QHsts) |
389 | |
390 | #include "tst_qhsts.moc" |
391 | |