qt5base-lts/tests/auto/network/access/hsts/tst_qhsts.cpp
Mårten Nordheim 1b736a815b Hsts: match header names case insensitively
Header field names are always considered to be case-insensitive.

Pick-to: 6.5 6.5.1 6.2 5.15
Fixes: QTBUG-113392
Change-Id: Ifb4def4bb7f2ac070416cdc76581a769f1e52b43
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
2023-05-08 20:28:46 +02:00

372 lines
14 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QtCore/qdatetime.h>
#include <QtCore/qdir.h>
#include <QtCore/qlist.h>
#include <QtCore/qpair.h>
#include <QtCore/qurl.h>
#include <QtNetwork/private/qhstsstore_p.h>
#include <QtNetwork/private/qhsts_p.h>
QT_USE_NAMESPACE
class tst_QHsts : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testSingleKnownHost_data();
void testSingleKnownHost();
void testMultilpeKnownHosts();
void testPolicyExpiration();
void testSTSHeaderParser();
void testStore();
};
void tst_QHsts::testSingleKnownHost_data()
{
QTest::addColumn<QUrl>("knownHost");
QTest::addColumn<QDateTime>("policyExpires");
QTest::addColumn<bool>("includeSubDomains");
QTest::addColumn<QUrl>("hostToTest");
QTest::addColumn<bool>("isKnown");
const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
const QUrl knownHost(QLatin1String("http://example.com"));
const QUrl validSubdomain(QLatin1String("https://sub.example.com/ohoho"));
const QUrl unknownDomain(QLatin1String("http://example.org"));
const QUrl subSubdomain(QLatin1String("https://level3.level2.example.com"));
const QDateTime validDate(currentUTC.addSecs(1000));
QTest::newRow("same-known") << knownHost << validDate << false << knownHost << true;
QTest::newRow("subexcluded") << knownHost << validDate << false << validSubdomain << false;
QTest::newRow("subincluded") << knownHost << validDate << true << validSubdomain << true;
QTest::newRow("unknown-subexcluded") << knownHost << validDate << false << unknownDomain << false;
QTest::newRow("unknown-subincluded") << knownHost << validDate << true << unknownDomain << false;
QTest::newRow("sub-subdomain-subincluded") << knownHost << validDate << true << subSubdomain << true;
QTest::newRow("sub-subdomain-subexcluded") << knownHost << validDate << false << subSubdomain << false;
const QDateTime invalidDate;
QTest::newRow("invalid-time") << knownHost << invalidDate << false << knownHost << false;
QTest::newRow("invalid-time-subexcluded") << knownHost << invalidDate << false
<< validSubdomain << false;
QTest::newRow("invalid-time-subincluded") << knownHost << invalidDate << true
<< validSubdomain << false;
const QDateTime expiredDate(currentUTC.addSecs(-1000));
QTest::newRow("expired-time") << knownHost << expiredDate << false << knownHost << false;
QTest::newRow("expired-time-subexcluded") << knownHost << expiredDate << false
<< validSubdomain << false;
QTest::newRow("expired-time-subincluded") << knownHost << expiredDate << true
<< validSubdomain << false;
const QUrl ipAsHost(QLatin1String("http://127.0.0.1"));
QTest::newRow("ip-address-in-hostname") << ipAsHost << validDate << false
<< ipAsHost << false;
const QUrl anyIPv4AsHost(QLatin1String("http://0.0.0.0"));
QTest::newRow("anyip4-address-in-hostname") << anyIPv4AsHost << validDate
<< false << anyIPv4AsHost << false;
const QUrl anyIPv6AsHost(QLatin1String("http://[::]"));
QTest::newRow("anyip6-address-in-hostname") << anyIPv6AsHost << validDate
<< false << anyIPv6AsHost << false;
}
void tst_QHsts::testSingleKnownHost()
{
QFETCH(const QUrl, knownHost);
QFETCH(const QDateTime, policyExpires);
QFETCH(const bool, includeSubDomains);
QFETCH(const QUrl, hostToTest);
QFETCH(const bool, isKnown);
QHstsCache cache;
cache.updateKnownHost(knownHost, policyExpires, includeSubDomains);
QCOMPARE(cache.isKnownHost(hostToTest), isKnown);
}
void tst_QHsts::testMultilpeKnownHosts()
{
const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
const QDateTime validDate(currentUTC.addSecs(10000));
const QDateTime expiredDate(currentUTC.addSecs(-10000));
const QUrl exampleCom(QLatin1String("https://example.com"));
const QUrl subExampleCom(QLatin1String("https://sub.example.com"));
QHstsCache cache;
// example.com is HSTS and includes subdomains:
cache.updateKnownHost(exampleCom, validDate, true);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subExampleCom));
// example.com can set its policy not to include subdomains:
cache.updateKnownHost(exampleCom, validDate, false);
QVERIFY(!cache.isKnownHost(subExampleCom));
// but sub.example.com can set its own policy:
cache.updateKnownHost(subExampleCom, validDate, false);
QVERIFY(cache.isKnownHost(subExampleCom));
// let's say example.com's policy has expired:
cache.updateKnownHost(exampleCom, expiredDate, false);
QVERIFY(!cache.isKnownHost(exampleCom));
// it should not affect sub.example.com's policy:
QVERIFY(cache.isKnownHost(subExampleCom));
// clear cache and invalidate all policies:
cache.clear();
QVERIFY(!cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subExampleCom));
// siblings:
const QUrl anotherSub(QLatin1String("https://sub2.example.com"));
cache.updateKnownHost(subExampleCom, validDate, true);
cache.updateKnownHost(anotherSub, validDate, true);
QVERIFY(cache.isKnownHost(subExampleCom));
QVERIFY(cache.isKnownHost(anotherSub));
// they cannot set superdomain's policy:
QVERIFY(!cache.isKnownHost(exampleCom));
// a sibling cannot set another sibling's policy:
cache.updateKnownHost(anotherSub, expiredDate, false);
QVERIFY(cache.isKnownHost(subExampleCom));
QVERIFY(!cache.isKnownHost(anotherSub));
QVERIFY(!cache.isKnownHost(exampleCom));
// let's make example.com known again:
cache.updateKnownHost(exampleCom, validDate, true);
// a subdomain cannot affect its superdomain's policy:
cache.updateKnownHost(subExampleCom, expiredDate, true);
QVERIFY(cache.isKnownHost(exampleCom));
// and this superdomain includes subdomains in its HSTS policy:
QVERIFY(cache.isKnownHost(subExampleCom));
QVERIFY(cache.isKnownHost(anotherSub));
// a subdomain (with its subdomains) cannot affect its superdomain's policy:
cache.updateKnownHost(exampleCom, expiredDate, true);
cache.updateKnownHost(subExampleCom, validDate, true);
QVERIFY(cache.isKnownHost(subExampleCom));
QVERIFY(!cache.isKnownHost(exampleCom));
}
void tst_QHsts::testPolicyExpiration()
{
QDateTime currentUTC = QDateTime::currentDateTimeUtc();
const QUrl exampleCom(QLatin1String("http://example.com"));
const QUrl subdomain(QLatin1String("http://subdomain.example.com"));
const qint64 lifeTimeMS = 50;
QHstsCache cache;
// start with 'includeSubDomains' and 5 s. lifetime:
cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS), true);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subdomain));
// wait for approx. a half of lifetime:
QTest::qWait(lifeTimeMS / 2);
if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) {
// Should still be valid:
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subdomain));
}
QTest::qWait(lifeTimeMS);
// expired:
QVERIFY(!cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subdomain));
// now check that superdomain's policy expires, but not subdomain's policy:
currentUTC = QDateTime::currentDateTimeUtc();
cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS / 5), true);
cache.updateKnownHost(subdomain, currentUTC.addMSecs(lifeTimeMS), true);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subdomain));
QTest::qWait(lifeTimeMS / 2);
if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) {
QVERIFY(!cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subdomain));
}
}
void tst_QHsts::testSTSHeaderParser()
{
QHstsHeaderParser parser;
using Header = QPair<QByteArray, QByteArray>;
using Headers = QList<Header>;
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
Headers list;
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
list << Header("Strict-Transport-security", "200");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
// This header is missing REQUIRED max-age directive, so we'll ignore it:
list << Header("Strict-Transport-Security", "includeSubDomains");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
list.pop_back();
list << Header("Strict-Transport-Security", "includeSubDomains;max-age=1000");
QVERIFY(parser.parse(list));
QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc());
QVERIFY(parser.includeSubDomains());
list.pop_back();
list << Header("strict-transport-security", "includeSubDomains;max-age=1000");
QVERIFY(parser.parse(list));
QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc());
QVERIFY(parser.includeSubDomains());
list.pop_back();
// Invalid (includeSubDomains twice):
list << Header("Strict-Transport-Security", "max-age = 1000 ; includeSubDomains;includeSubDomains");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
list.pop_back();
// Invalid (weird number of seconds):
list << Header("Strict-Transport-Security", "max-age=-1000 ; includeSubDomains");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
list.pop_back();
// Note, directives are case-insensitive + we should ignore unknown directive.
list << Header("Strict-Transport-Security", ";max-age=1000 ;includesubdomains;;"
"nowsomeunknownheader=\"somevaluewithescapes\\;\"");
QVERIFY(parser.parse(list));
QVERIFY(parser.includeSubDomains());
QVERIFY(parser.expirationDate().isValid());
list.pop_back();
// Check that we know how to unescape max-age:
list << Header("Strict-Transport-Security", "max-age=\"1000\"");
QVERIFY(parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(parser.expirationDate().isValid());
list.pop_back();
// The only STS header, with invalid syntax though, to be ignored:
list << Header("Strict-Transport-Security", "max-age; max-age=15768000");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
// Now we check that our parse chosses the first valid STS header and ignores
// others:
list.clear();
list << Header("Strict-Transport-Security", "includeSubdomains; max-age=\"hehehe\";");
list << Header("Strict-Transport-Security", "max-age=10101");
QVERIFY(parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(parser.expirationDate().isValid());
list.clear();
list << Header("Strict-Transport-Security", "max-age=0");
QVERIFY(parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(parser.expirationDate() <= QDateTime::currentDateTimeUtc());
// Parsing is case-insensitive:
list.pop_back();
list << Header("Strict-Transport-Security", "Max-aGE=1000; InclUdesUbdomains");
QVERIFY(parser.parse(list));
QVERIFY(parser.includeSubDomains());
QVERIFY(parser.expirationDate().isValid());
// Grammar of STS header is quite permissive, let's check we can parse
// some weird but valid header:
list.pop_back();
list << Header("Strict-Transport-Security", ";;; max-age = 17; ; ; ; ;;; ;;"
";;; ; includeSubdomains ;;thisIsUnknownDirective;;;;");
QVERIFY(parser.parse(list));
QVERIFY(parser.includeSubDomains());
QVERIFY(parser.expirationDate().isValid());
list.pop_back();
list << Header("Strict-Transport-Security", "max-age=1000; includeSubDomains bogon");
QVERIFY(!parser.parse(list));
QVERIFY(!parser.includeSubDomains());
QVERIFY(!parser.expirationDate().isValid());
}
const QLatin1String storeDir(".");
struct TestStoreDeleter
{
~TestStoreDeleter()
{
QDir cwd;
if (!cwd.remove(QHstsStore::absoluteFilePath(storeDir)))
qWarning() << "tst_QHsts::testStore: failed to remove the hsts store file";
}
};
void tst_QHsts::testStore()
{
// Delete the store's file after we finish the test.
TestStoreDeleter cleaner;
const QUrl exampleCom(QStringLiteral("http://example.com"));
const QUrl subDomain(QStringLiteral("http://subdomain.example.com"));
const QDateTime validDate(QDateTime::currentDateTimeUtc().addDays(1));
{
// We start from an empty cache and empty store:
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(!cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subDomain));
// (1) This will also store the policy:
cache.updateKnownHost(exampleCom, validDate, true);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subDomain));
}
{
// Test the policy stored at (1):
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(cache.isKnownHost(subDomain));
// (2) Remove subdomains:
cache.updateKnownHost(exampleCom, validDate, false);
QVERIFY(!cache.isKnownHost(subDomain));
}
{
// Test the previous update (2):
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(exampleCom));
QVERIFY(!cache.isKnownHost(subDomain));
}
{
QHstsCache cache;
cache.updateKnownHost(subDomain, validDate, false);
QVERIFY(cache.isKnownHost(subDomain));
QHstsStore store(storeDir);
// (3) This should store policy from cache, over old policy from store:
cache.setStore(&store);
}
{
// Test that (3) was stored:
QHstsCache cache;
QHstsStore store(storeDir);
cache.setStore(&store);
QVERIFY(cache.isKnownHost(subDomain));
}
}
QTEST_MAIN(tst_QHsts)
#include "tst_qhsts.moc"