aboutsummaryrefslogtreecommitdiffstats
path: root/src/requestinterceptor.cpp
blob: 8fd24197721ca22da265c126e1bfc3b60eb37c73 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "requestinterceptor.h"

#include <QWebEngineUrlRequestInfo>

RequestInterceptor::RequestInterceptor(QObject *parent)
    : QWebEngineUrlRequestInterceptor(parent)
{
}

bool RequestInterceptor::shouldAllow(const QUrl &url)
{
    // QUrl::scheme() always normalizes to lowercase (verified: QUrl("HTTP://x/y")
    // .scheme() == "http"), so a lowercase-literal compare cannot be bypassed
    // by unusual casing, in either the allow or the deny direction.
    const QString scheme = url.scheme();

    // The document itself is loaded via setHtml() with a qtmaildir: base URL,
    // so a request for exactly that URL must pass or nothing renders at all.
    // This is the ONLY trusted qtmaildir: URL: everything else on this scheme
    // is denied, including sub-paths of it. A hostile message body can put
    // arbitrary qtmaildir: URLs in <img src>, <link href>, etc., so this
    // cannot be a whole-scheme allow; it must be an exact match against the
    // one URL the application itself chose. If setDocumentUrl() was never
    // called, m_documentUrl is a default-constructed (invalid, empty) QUrl,
    // which cannot equal any real request URL, so this fails closed.
    if (scheme == QLatin1String("qtmaildir")) {
        if (!m_documentUrl.isEmpty() && url == m_documentUrl)
            return true;
        m_blockedAnything = true;
        return false;
    }

    // Inline parts of the current message only.
    if (scheme == QLatin1String("cid")) {
        // QUrl keeps a cid: body in path(), not host() or userName(), even
        // when it contains '@' (verified empirically: QUrl("cid:logo@example.org")
        // .path() == "logo@example.org", host() and userName() are empty).
        // path() also returns the percent-decoded form, so a percent-encoded
        // id (e.g. "%6Cogo@example.org") compares equal to its decoded form,
        // not to some other allowed id: it cannot be used to smuggle a
        // foreign id past the allowlist, only to spell an already-legitimate
        // id differently.
        const QString id = url.path();
        if (m_allowedCids.contains(id))
            return true;
        m_blockedAnything = true;
        return false;
    }

    if (scheme == QLatin1String("http") || scheme == QLatin1String("https")) {
        if (m_allowRemote)
            return true;
        m_blockedAnything = true;
        return false;
    }

    // Everything else, including file:, javascript:, data:, blob:, about:,
    // chrome:, qrc:, filesystem:, protocol-relative URLs (empty scheme with a
    // host), and empty/malformed URLs (empty scheme), is denied
    // unconditionally. There is no flag that enables it.
    m_blockedAnything = true;
    return false;
}

void RequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info)
{
    if (!shouldAllow(info.requestUrl()))
        info.block(true);
}

void RequestInterceptor::resetForNewMessage()
{
    m_allowRemote = false;
    m_blockedAnything = false;
}