File indexing completed on 2025-03-09 04:54:38
0001 /* 0002 SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 0006 */ 0007 #include "scamdetectionwebengine.h" 0008 #include "MessageViewer/ScamCheckShortUrl" 0009 #include "scamdetectiondetailsdialog.h" 0010 #include "settings/messageviewersettings.h" 0011 #include "webengineviewer/webenginescript.h" 0012 #include <WebEngineViewer/WebEngineManageScript> 0013 0014 #include <KLocalizedString> 0015 0016 #include <QPointer> 0017 #include <QRegularExpression> 0018 #include <QWebEnginePage> 0019 0020 using namespace MessageViewer; 0021 0022 template<typename Arg, typename R, typename C> 0023 struct InvokeWrapper { 0024 QPointer<R> receiver; 0025 void (C::*memberFunction)(Arg); 0026 void operator()(Arg result) 0027 { 0028 if (receiver) { 0029 (receiver->*memberFunction)(result); 0030 } 0031 } 0032 }; 0033 0034 template<typename Arg, typename R, typename C> 0035 0036 InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFunction)(Arg)) 0037 { 0038 InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFunction}; 0039 return wrapper; 0040 } 0041 0042 static QString addWarningColor(const QString &url) 0043 { 0044 const QString error = QStringLiteral("<font color=#FF0000>%1</font>").arg(url); 0045 return error; 0046 } 0047 0048 class MessageViewer::ScamDetectionWebEnginePrivate 0049 { 0050 public: 0051 ScamDetectionWebEnginePrivate() = default; 0052 0053 QString mDetails; 0054 QPointer<MessageViewer::ScamDetectionDetailsDialog> mDetailsDialog; 0055 }; 0056 0057 ScamDetectionWebEngine::ScamDetectionWebEngine(QObject *parent) 0058 : QObject(parent) 0059 , d(new MessageViewer::ScamDetectionWebEnginePrivate) 0060 { 0061 } 0062 0063 ScamDetectionWebEngine::~ScamDetectionWebEngine() = default; 0064 0065 void ScamDetectionWebEngine::scanPage(QWebEnginePage *page) 0066 { 0067 if (MessageViewer::MessageViewerSettings::self()->scamDetectionEnabled()) { 0068 page->runJavaScript(WebEngineViewer::WebEngineScript::findAllAnchorsAndForms(), 0069 WebEngineViewer::WebEngineManageScript::scriptWordId(), 0070 invoke(this, &ScamDetectionWebEngine::handleScanPage)); 0071 } 0072 } 0073 0074 void ScamDetectionWebEngine::handleScanPage(const QVariant &result) 0075 { 0076 bool foundScam = false; 0077 0078 d->mDetails.clear(); 0079 const QVariantList resultList = result.toList(); 0080 if (resultList.count() != 1) { 0081 Q_EMIT resultScanDetection(foundScam); 0082 return; 0083 } 0084 static const QRegularExpression ip4regExp(QStringLiteral("\\b[0-9]{1,3}\\.[0-9]{1,3}(?:\\.[0-9]{0,3})?(?:\\.[0-9]{0,3})?")); 0085 const QVariantMap mapResult = resultList.at(0).toMap(); 0086 const QList<QVariant> lst = mapResult.value(QStringLiteral("anchors")).toList(); 0087 for (const QVariant &var : lst) { 0088 QMap<QString, QVariant> mapVariant = var.toMap(); 0089 // qDebug()<<" mapVariant"<<mapVariant; 0090 0091 // 1) detect if title has a url and title != href 0092 const QString title = mapVariant.value(QStringLiteral("title")).toString(); 0093 QString href = mapVariant.value(QStringLiteral("src")).toString(); 0094 if (!QUrl(href).toString().contains(QLatin1StringView("kmail:showAuditLog"))) { 0095 href = href.toLower(); 0096 } 0097 const QUrl url(href); 0098 if (!title.isEmpty()) { 0099 if (title.startsWith(QLatin1StringView("http:")) || title.startsWith(QLatin1StringView("https:")) || title.startsWith(QLatin1StringView("www."))) { 0100 if (title.startsWith(QLatin1StringView("www."))) { 0101 const QString completUrl = url.scheme() + QLatin1StringView("://") + title; 0102 if (completUrl != href && href != (completUrl + QLatin1Char('/'))) { 0103 foundScam = true; 0104 } 0105 } else { 0106 if (href != title) { 0107 // http://www.kde.org == http://www.kde.org/ 0108 if (href != (title + QLatin1Char('/'))) { 0109 foundScam = true; 0110 } 0111 } 0112 } 0113 if (foundScam) { 0114 d->mDetails += QLatin1StringView("<li>") 0115 + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is often the case in scam emails " 0116 "to mislead the recipient", 0117 addWarningColor(title), 0118 addWarningColor(href)) 0119 + QLatin1StringView("</li>"); 0120 } 0121 } 0122 } 0123 if (!foundScam) { 0124 // 2) detect if url href has ip and not server name. 0125 const QString hostname = url.host(); 0126 if (hostname.contains(ip4regExp) && !hostname.contains(QLatin1StringView("127.0.0.1"))) { // hostname 0127 d->mDetails += QLatin1StringView("<li>") 0128 + i18n("This email contains a link which points to a numerical IP address (%1) instead of a typical textual website address. This is often " 0129 "the case in scam emails.", 0130 addWarningColor(hostname)) 0131 + QLatin1StringView("</li>"); 0132 foundScam = true; 0133 } else if (hostname.contains(QLatin1Char('%'))) { // Hexa value for ip 0134 d->mDetails += QLatin1StringView("<li>") 0135 + i18n("This email contains a link which points to a hexadecimal IP address (%1) instead of a typical textual website address. This is " 0136 "often the case in scam emails.", 0137 addWarningColor(hostname)) 0138 + QLatin1StringView("</li>"); 0139 foundScam = true; 0140 } else if (url.toString().contains(QLatin1StringView("url?q="))) { // 4) redirect url. 0141 d->mDetails += QLatin1StringView("<li>") + i18n("This email contains a link (%1) which has a redirection", addWarningColor(url.toString())) 0142 + QLatin1StringView("</li>"); 0143 foundScam = true; 0144 } else if ((url.toString().count(QStringLiteral("http://")) > 1) 0145 || (url.toString().count(QStringLiteral("https://")) > 1)) { // 5) more that 1 http in url. 0146 if (!url.toString().contains(QLatin1StringView("kmail:showAuditLog"))) { 0147 d->mDetails += QLatin1StringView("<li>") 0148 + i18n("This email contains a link (%1) which contains multiple http://. This is often the case in scam emails.", 0149 addWarningColor(url.toString())) 0150 + QLatin1StringView("</li>"); 0151 foundScam = true; 0152 } 0153 } 0154 } 0155 // Check shortUrl 0156 if (!foundScam) { 0157 if (ScamCheckShortUrl::isShortUrl(url)) { 0158 d->mDetails += QLatin1StringView("<li>") 0159 + i18n("This email contains a shorturl (%1). It can redirect to another server.", addWarningColor(url.toString())) 0160 + QLatin1StringView("</li>"); 0161 foundScam = true; 0162 } 0163 } 0164 if (!foundScam) { 0165 QUrl displayUrl = QUrl(mapVariant.value(QStringLiteral("text")).toString()); 0166 // Special case if https + port 443 it will return url without port 0167 QString text = (displayUrl.port() == 443 && displayUrl.scheme() == QLatin1StringView("https")) 0168 ? displayUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments | QUrl::RemovePort) 0169 : displayUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments); 0170 if (text.endsWith(QLatin1StringView("%22"))) { 0171 text.chop(3); 0172 } 0173 const QUrl normalizedHrefUrl = QUrl(href.toLower()); 0174 QString normalizedHref = normalizedHrefUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments); 0175 if (text != normalizedHref) { 0176 if (normalizedHref.contains(QStringLiteral("%5C"))) { 0177 normalizedHref.replace(QStringLiteral("%5C"), QStringLiteral("/")); 0178 } 0179 } 0180 if (normalizedHref.endsWith(QLatin1StringView("%22"))) { 0181 normalizedHref.chop(3); 0182 } 0183 // qDebug() << "text " << text << " href "<<href << " normalizedHref " << normalizedHref; 0184 0185 if (!text.isEmpty()) { 0186 if (text.startsWith(QLatin1StringView("http:/")) || text.startsWith(QLatin1StringView("https:/"))) { 0187 if (text.toLower() != normalizedHref.toLower()) { 0188 if (text != normalizedHref) { 0189 if (normalizedHref != (text + QLatin1Char('/'))) { 0190 if (normalizedHref.toHtmlEscaped() != text) { 0191 if (QString::fromUtf8(QUrl(text).toEncoded()) != normalizedHref) { 0192 if (QUrl(normalizedHref).toDisplayString() != text) { 0193 const bool qurlqueryequal = displayUrl.query() == normalizedHrefUrl.query(); 0194 const QString displayUrlWithoutQuery = 0195 displayUrl.toDisplayString(QUrl::RemoveQuery | QUrl::StripTrailingSlash | QUrl::NormalizePathSegments); 0196 const QString hrefUrlWithoutQuery = 0197 normalizedHrefUrl.toDisplayString(QUrl::RemoveQuery | QUrl::StripTrailingSlash | QUrl::NormalizePathSegments); 0198 // qDebug() << "displayUrlWithoutQuery " << displayUrlWithoutQuery << " hrefUrlWithoutQuery " << 0199 // hrefUrlWithoutQuery << " text " << text; 0200 if (qurlqueryequal && (displayUrlWithoutQuery + QLatin1Char('/') != hrefUrlWithoutQuery)) { 0201 d->mDetails += QLatin1StringView("<li>") 0202 + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is " 0203 "often " 0204 "the case in scam emails to mislead the recipient", 0205 addWarningColor(text), 0206 addWarningColor(normalizedHref)) 0207 + QLatin1StringView("</li>"); 0208 foundScam = true; 0209 } 0210 } 0211 } 0212 } 0213 } 0214 } 0215 } 0216 } 0217 } 0218 } 0219 } 0220 if (mapResult.value(QStringLiteral("forms")).toInt() > 0) { 0221 d->mDetails += 0222 QLatin1StringView("<li></b>") + i18n("Message contains form element. This is often the case in scam emails.") + QLatin1StringView("</b></li>"); 0223 foundScam = true; 0224 } 0225 if (foundScam) { 0226 d->mDetails.prepend(QLatin1StringView("<b>") + i18n("Details:") + QLatin1StringView("</b><ul>")); 0227 d->mDetails += QLatin1StringView("</ul>"); 0228 Q_EMIT messageMayBeAScam(); 0229 } 0230 Q_EMIT resultScanDetection(foundScam); 0231 } 0232 0233 void ScamDetectionWebEngine::showDetails() 0234 { 0235 if (!d->mDetailsDialog) { 0236 d->mDetailsDialog = new MessageViewer::ScamDetectionDetailsDialog; 0237 } 0238 d->mDetailsDialog->setDetails(d->mDetails); 0239 d->mDetailsDialog->show(); 0240 } 0241 0242 #include "moc_scamdetectionwebengine.cpp"