aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_interceptor.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_interceptor.cpp')
-rw-r--r--tests/test_interceptor.cpp228
1 files changed, 228 insertions, 0 deletions
diff --git a/tests/test_interceptor.cpp b/tests/test_interceptor.cpp
new file mode 100644
index 0000000..498a997
--- /dev/null
+++ b/tests/test_interceptor.cpp
@@ -0,0 +1,228 @@
+#include <QtTest>
+#include "requestinterceptor.h"
+
+class TestInterceptor : public QObject
+{
+ Q_OBJECT
+private slots:
+ void blocksRemoteHttpByDefault();
+ void blocksRemoteHttpsByDefault();
+ void blocksFileUrlsAlways();
+ void allowsCidForCurrentMessage();
+ void blocksCidForForeignMessage();
+ void allowRemoteFlagPermitsHttpButNotFile();
+ void recordsThatSomethingWasBlocked();
+ void resetClearsBlockedFlag();
+
+ // Adversarial additions.
+ void schemeIsCaseInsensitiveAndStillBlocked();
+ void qtmaildirSchemeIsCaseInsensitiveAllow();
+ void cidUrlDoesNotParseAsUserinfo();
+ void cidPercentEncodingDoesNotBypassAllowlist();
+ void javascriptSchemeBlocked();
+ void dataSchemeBlocked();
+ void blobSchemeBlocked();
+ void aboutSchemeBlocked();
+ void chromeSchemeBlocked();
+ void qrcSchemeBlocked();
+ void filesystemSchemeBlocked();
+ void protocolRelativeUrlBlocked();
+ void emptyUrlBlocked();
+ void blankUrlBlocked();
+ void colonOnlyUrlBlocked();
+ void fragmentOnlyUrlBlocked();
+};
+
+void TestInterceptor::blocksRemoteHttpByDefault()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("http://tracker.example/pixel.gif"))));
+}
+
+void TestInterceptor::blocksRemoteHttpsByDefault()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("https://cdn.example/style.css"))));
+}
+
+void TestInterceptor::blocksFileUrlsAlways()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ // Even with remote content explicitly allowed, local files stay blocked:
+ // a message must never read the filesystem.
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("file:///etc/passwd"))));
+}
+
+void TestInterceptor::allowsCidForCurrentMessage()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowedCids({ QStringLiteral("logo@example.org") });
+ QVERIFY(interceptor.shouldAllow(QUrl(QStringLiteral("cid:logo@example.org"))));
+}
+
+void TestInterceptor::blocksCidForForeignMessage()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowedCids({ QStringLiteral("logo@example.org") });
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("cid:other@example.org"))));
+}
+
+void TestInterceptor::allowRemoteFlagPermitsHttpButNotFile()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ QVERIFY(interceptor.shouldAllow(QUrl(QStringLiteral("https://cdn.example/img.png"))));
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("file:///etc/passwd"))));
+}
+
+void TestInterceptor::recordsThatSomethingWasBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.blockedAnything());
+ interceptor.shouldAllow(QUrl(QStringLiteral("http://tracker.example/p.gif")));
+ // Drives the "Remote content blocked" banner in the message header.
+ QVERIFY(interceptor.blockedAnything());
+}
+
+void TestInterceptor::resetClearsBlockedFlag()
+{
+ RequestInterceptor interceptor;
+ interceptor.shouldAllow(QUrl(QStringLiteral("http://tracker.example/p.gif")));
+ QVERIFY(interceptor.blockedAnything());
+
+ interceptor.resetForNewMessage();
+ QVERIFY(!interceptor.blockedAnything());
+ // Remote permission never carries over to the next message.
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("https://cdn.example/x.png"))));
+}
+
+void TestInterceptor::schemeIsCaseInsensitiveAndStillBlocked()
+{
+ // QUrl::scheme() normalizes to lowercase, so "HTTP://..." must still hit
+ // the http branch (and be blocked without allowRemote), not fall through
+ // unexpectedly to an allow path.
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("HTTP://tracker.example/pixel.gif"))));
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("HTTPS://tracker.example/pixel.gif"))));
+
+ interceptor.setAllowRemote(true);
+ QVERIFY(interceptor.shouldAllow(QUrl(QStringLiteral("HTTP://tracker.example/pixel.gif"))));
+}
+
+void TestInterceptor::qtmaildirSchemeIsCaseInsensitiveAllow()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(interceptor.shouldAllow(QUrl(QStringLiteral("QTMAILDIR://body/index.html"))));
+}
+
+void TestInterceptor::cidUrlDoesNotParseAsUserinfo()
+{
+ // Pin down QUrl's actual parsing of a cid: URL containing '@', so a
+ // future Qt version change would be caught here rather than silently
+ // breaking the allowlist comparison in shouldAllow().
+ QUrl url(QStringLiteral("cid:logo@example.org"));
+ QCOMPARE(url.path(), QStringLiteral("logo@example.org"));
+ QCOMPARE(url.host(), QString());
+ QCOMPARE(url.userName(), QString());
+
+ RequestInterceptor interceptor;
+ interceptor.setAllowedCids({ QStringLiteral("logo@example.org") });
+ QVERIFY(interceptor.shouldAllow(url));
+}
+
+void TestInterceptor::cidPercentEncodingDoesNotBypassAllowlist()
+{
+ // QUrl::path() returns the percent-DECODED form (verified empirically:
+ // QUrl("cid:%6Cogo@example.org").path() == "logo@example.org", and this
+ // holds for full-string encodings too). A percent-encoded spelling of an
+ // allowed id therefore compares equal to that same allowed id, which is
+ // correct URI equivalence, not a bypass: decoding cannot turn a foreign
+ // id into a *different* allowed id's literal string, only into its own
+ // canonical form. What matters for the security boundary is that a
+ // genuinely foreign id, encoded or not, is still rejected.
+ RequestInterceptor interceptor;
+ interceptor.setAllowedCids({ QStringLiteral("logo@example.org") });
+ QVERIFY(interceptor.shouldAllow(QUrl(QStringLiteral("cid:%6Cogo@example.org"))));
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("cid:other@example.org"))));
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("cid:%6Fther@example.org"))));
+}
+
+void TestInterceptor::javascriptSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("javascript:alert(1)"))));
+}
+
+void TestInterceptor::dataSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("data:text/html,<script>alert(1)</script>"))));
+}
+
+void TestInterceptor::blobSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("blob:https://example.org/uuid"))));
+}
+
+void TestInterceptor::aboutSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("about:blank"))));
+}
+
+void TestInterceptor::chromeSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("chrome://settings"))));
+}
+
+void TestInterceptor::qrcSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("qrc:/icons/foo.png"))));
+}
+
+void TestInterceptor::filesystemSchemeBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("filesystem:https://example.org/temporary/foo"))));
+}
+
+void TestInterceptor::protocolRelativeUrlBlocked()
+{
+ RequestInterceptor interceptor;
+ interceptor.setAllowRemote(true);
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("//tracker.example/pixel.gif"))));
+}
+
+void TestInterceptor::emptyUrlBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl()));
+}
+
+void TestInterceptor::blankUrlBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral(""))));
+}
+
+void TestInterceptor::colonOnlyUrlBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral(":"))));
+}
+
+void TestInterceptor::fragmentOnlyUrlBlocked()
+{
+ RequestInterceptor interceptor;
+ QVERIFY(!interceptor.shouldAllow(QUrl(QStringLiteral("#fragment"))));
+}
+
+QTEST_MAIN(TestInterceptor)
+#include "test_interceptor.moc"