Branch data Line data Source code
1 : : #include "ExternalContentInterceptor.h"
2 : :
3 : : #include <QLoggingCategory>
4 : : #include <QMutexLocker>
5 : : #include <QUrl>
6 : :
7 [ + - + - : 2 : Q_LOGGING_CATEGORY(lcInterceptor, "mailjd.interceptor")
+ - - - ]
8 : :
9 : 26 : ExternalContentInterceptor::ExternalContentInterceptor(QObject *parent)
10 : 26 : : QWebEngineUrlRequestInterceptor(parent) {}
11 : :
12 : 18 : void ExternalContentInterceptor::setBlockExternal(bool block) {
13 : 18 : QMutexLocker locker(&m_mutex);
14 : 18 : m_blockExternal = block;
15 : 18 : m_hasBlocked = false;
16 : 18 : m_blockedDomains.clear(); // T-122: Reset on new mail
17 : 18 : }
18 : :
19 : 3 : bool ExternalContentInterceptor::isBlocking() const {
20 : 3 : QMutexLocker locker(&m_mutex);
21 : 6 : return m_blockExternal;
22 : 3 : }
23 : :
24 : : // T-122: Whitelist support
25 : 57 : void ExternalContentInterceptor::setWhitelist(const QStringList &domains,
26 : : const QStringList &senders) {
27 : : Q_UNUSED(senders);
28 : 57 : QMutexLocker locker(&m_mutex);
29 : 57 : m_whitelistedDomains.clear();
30 [ + + ]: 85 : for (const auto &d : domains)
31 [ + - + - ]: 28 : m_whitelistedDomains.insert(d.toLower());
32 : 57 : }
33 : :
34 : 3 : void ExternalContentInterceptor::setSenderEmail(const QString &email) {
35 : : Q_UNUSED(email);
36 : 3 : }
37 : :
38 : 13 : bool ExternalContentInterceptor::isAllowedByWhitelist(
39 : : const QUrl &requestUrl) const {
40 : 13 : QMutexLocker locker(&m_mutex);
41 [ + - ]: 26 : return isAllowedByWhitelistLocked(requestUrl);
42 : 13 : }
43 : :
44 : 47 : void ExternalContentInterceptor::resetBlockedDomains() {
45 : 47 : QMutexLocker locker(&m_mutex);
46 : 47 : m_blockedDomains.clear();
47 : 47 : }
48 : :
49 : 6 : QSet<QString> ExternalContentInterceptor::blockedDomains() const {
50 : 6 : QMutexLocker locker(&m_mutex);
51 : 6 : return m_blockedDomains;
52 : 6 : }
53 : :
54 : 53 : void ExternalContentInterceptor::interceptRequest(
55 : : QWebEngineUrlRequestInfo &info) {
56 : 53 : bool emitBlocked = false;
57 : 53 : bool emitLink = false;
58 [ + - ]: 53 : QUrl signalUrl;
59 [ + - ]: 53 : QUrl blockedUrl;
60 : :
61 : : {
62 : : // T-616/SEC-15: Guard shared state access — this runs on Chromium IO thread.
63 : : // Signals are emitted after the mutex is released; connected UI slots may
64 : : // synchronously call getters such as blockedDomains().
65 : 53 : QMutexLocker locker(&m_mutex);
66 : :
67 : : // T-350: Intercept link click navigations (main frame only).
68 : : // Block the navigation and emit signal so MailView opens it in the
69 : : // system browser. This replaces MailWebEnginePage::acceptNavigationRequest
70 : : // and eliminates the need for setPage() (which caused a freeze).
71 [ + - + + : 66 : if (info.navigationType() == QWebEngineUrlRequestInfo::NavigationTypeLink &&
- + ]
72 [ + - - + ]: 13 : info.resourceType() == QWebEngineUrlRequestInfo::ResourceTypeMainFrame) {
73 [ # # ]: 0 : info.block(true);
74 [ # # ]: 0 : signalUrl = info.requestUrl();
75 : 0 : emitLink = true;
76 : : } else {
77 [ + + ]: 53 : if (!m_blockExternal)
78 : 51 : return;
79 : :
80 [ + - + - : 34 : QString scheme = info.requestUrl().scheme().toLower();
+ - ]
81 [ + + + - : 97 : if (scheme != QStringLiteral("http") && scheme != QStringLiteral("https"))
+ + + + +
- + - +
+ ]
82 : 29 : return;
83 : :
84 : : // T-122/MED-16: Only the requested resource domain is trusted here.
85 : : // The displayed From sender/domain is spoofable without message auth
86 : : // results, so it must not auto-unblock external content.
87 [ + - + - : 10 : QString requestDomain = info.requestUrl().host().toLower();
+ - ]
88 [ + - + - : 5 : if (isAllowedByWhitelistLocked(info.requestUrl()))
+ + ]
89 : 3 : return;
90 : :
91 : : // Block and track
92 [ + - ]: 2 : info.block(true);
93 [ + - ]: 2 : m_blockedDomains.insert(requestDomain); // T-122: Track blocked domain
94 [ + - ]: 2 : blockedUrl = info.requestUrl();
95 : :
96 : : // Signal once per page load to show the info bar
97 [ + - ]: 2 : if (!m_hasBlocked) {
98 : 2 : m_hasBlocked = true;
99 : 2 : emitBlocked = true;
100 : : }
101 [ + + + + ]: 37 : }
102 [ + + ]: 53 : }
103 : :
104 [ - + ]: 2 : if (emitLink)
105 [ # # ]: 0 : emit linkClicked(signalUrl);
106 : :
107 [ + - + - ]: 2 : if (!blockedUrl.isEmpty())
108 [ + - + - : 4 : qCInfo(lcInterceptor) << "Blocked external request:"
+ - + + ]
109 [ + - + - ]: 2 : << blockedUrl.toString();
110 : :
111 [ + - ]: 2 : if (emitBlocked)
112 [ + - ]: 2 : emit externalContentBlocked();
113 [ + + + + ]: 104 : }
114 : :
115 : : #ifdef MAILJD_UNIT_TEST
116 : 1 : void ExternalContentInterceptor::simulateBlockedExternalRequestForTest(
117 : : const QUrl &requestUrl) {
118 : 1 : bool emitBlocked = false;
119 : : {
120 : 1 : QMutexLocker locker(&m_mutex);
121 [ + - + - : 1 : m_blockedDomains.insert(requestUrl.host().toLower());
+ - ]
122 [ + - ]: 1 : if (!m_hasBlocked) {
123 : 1 : m_hasBlocked = true;
124 : 1 : emitBlocked = true;
125 : : }
126 : 1 : }
127 : :
128 [ + - ]: 1 : if (emitBlocked)
129 : 1 : emit externalContentBlocked();
130 : 1 : }
131 : : #endif
132 : :
133 : 18 : bool ExternalContentInterceptor::isAllowedByWhitelistLocked(
134 : : const QUrl &requestUrl) const {
135 [ + - + - ]: 18 : const QString requestDomain = requestUrl.host().toLower();
136 [ + + ]: 18 : if (requestDomain.isEmpty())
137 : 2 : return false;
138 [ + + ]: 16 : if (m_whitelistedDomains.contains(requestDomain))
139 : 4 : return true;
140 : : // 67.A4: A whitelist entry also covers its subdomains — the user who
141 : : // allowed "example.com" expects images from "img.example.com" / a CDN
142 : : // host of the same domain to load. The dot boundary prevents
143 : : // "evilexample.com" from matching the entry "example.com".
144 : : // MED-16 still holds: only user-curated whitelist entries are
145 : : // consulted; the spoofable From sender never auto-unblocks anything.
146 [ + + ]: 18 : for (const auto &entry : m_whitelistedDomains) {
147 [ + - + - : 9 : if (requestDomain.endsWith(QLatin1Char('.') + entry))
+ + ]
148 : 3 : return true;
149 : : }
150 : 9 : return false;
151 : 18 : }
|