diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/modules/PhishingDetector.jsm | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/modules/PhishingDetector.jsm')
-rw-r--r-- | comm/mail/modules/PhishingDetector.jsm | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/comm/mail/modules/PhishingDetector.jsm b/comm/mail/modules/PhishingDetector.jsm new file mode 100644 index 0000000000..016530fd96 --- /dev/null +++ b/comm/mail/modules/PhishingDetector.jsm @@ -0,0 +1,335 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["PhishingDetector"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + isLegalIPAddress: "resource:///modules/hostnameUtils.jsm", + isLegalLocalIPAddress: "resource:///modules/hostnameUtils.jsm", +}); + +const PhishingDetector = new (class PhishingDetector { + mEnabled = true; + mCheckForIPAddresses = true; + mCheckForMismatchedHosts = true; + mDisallowFormActions = true; + + constructor() { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "mEnabled", + "mail.phishing.detection.enabled", + true + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "mCheckForIPAddresses", + "mail.phishing.detection.ipaddresses", + true + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "mCheckForMismatchedHosts", + "mail.phishing.detection.mismatched_hosts", + true + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "mDisallowFormActions", + "mail.phishing.detection.disallow_form_actions", + true + ); + } + + /** + * Analyze the currently loaded message in the message pane, looking for signs + * of a phishing attempt. Also checks for forms with action URLs, which are + * disallowed. + * Assumes the message has finished loading in the message pane (i.e. + * OnMsgParsed has fired). + * + * @param {nsIMsgMailNewsUrl} aUrl + * Url for the message being analyzed. + * @param {Element} browser + * The browser element where the message is loaded. + * @returns {boolean} + * Returns true if this does have phishing urls. Returns false if we + * do not check this message or the phishing message does not need to be + * displayed. + */ + analyzeMsgForPhishingURLs(aUrl, browser) { + if (!aUrl || !this.mEnabled) { + return false; + } + + try { + // nsIMsgMailNewsUrl.folder can throw an NS_ERROR_FAILURE, especially if + // we are opening an .eml file. + var folder = aUrl.folder; + + // Ignore nntp and RSS messages. + if ( + !folder || + folder.server.type == "nntp" || + folder.server.type == "rss" + ) { + return false; + } + + // Also ignore messages in Sent/Drafts/Templates/Outbox. + let outgoingFlags = + Ci.nsMsgFolderFlags.SentMail | + Ci.nsMsgFolderFlags.Drafts | + Ci.nsMsgFolderFlags.Templates | + Ci.nsMsgFolderFlags.Queue; + if (folder.isSpecialFolder(outgoingFlags, true)) { + return false; + } + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FAILURE) { + throw ex; + } + } + + // If the message contains forms with action attributes, warn the user. + let formNodes = browser.contentDocument.querySelectorAll("form[action]"); + + return this.mDisallowFormActions && formNodes.length > 0; + } + + /** + * Analyze the url contained in aLinkNode for phishing attacks. + * + * @param {string} aHref - the url to be analyzed + * @param {string} [aLinkText] - user visible link text associated with aHref + * in case we are dealing with a link node. + * @returns true if link node contains phishing URL. false otherwise. + */ + #analyzeUrl(aUrl, aLinkText) { + if (!aUrl) { + return false; + } + + let hrefURL; + // make sure relative link urls don't make us bail out + try { + hrefURL = Services.io.newURI(aUrl); + } catch (ex) { + return false; + } + + // only check for phishing urls if the url is an http or https link. + // this prevents us from flagging imap and other internally handled urls + if (hrefURL.schemeIs("http") || hrefURL.schemeIs("https")) { + // The link is not suspicious if the visible text is the same as the URL, + // even if the URL is an IP address. URLs are commonly surrounded by + // < > or "" (RFC2396E) - so strip those from the link text before comparing. + if (aLinkText) { + aLinkText = aLinkText.replace(/^<(.+)>$|^"(.+)"$/, "$1$2"); + } + + var failsStaticTests = false; + // If the link text and url differs by something other than a trailing + // slash, do some further checks. + if ( + aLinkText && + aLinkText != aUrl && + aLinkText.replace(/\/+$/, "") != aUrl.replace(/\/+$/, "") + ) { + if (this.mCheckForIPAddresses) { + let unobscuredHostNameValue = lazy.isLegalIPAddress( + hrefURL.host, + true + ); + if (unobscuredHostNameValue) { + failsStaticTests = !lazy.isLegalLocalIPAddress( + unobscuredHostNameValue + ); + } + } + + if (!failsStaticTests && this.mCheckForMismatchedHosts) { + failsStaticTests = + aLinkText && this.misMatchedHostWithLinkText(hrefURL, aLinkText); + } + } + // We don't use dynamic checks anymore. The old implementation was removed + // in bug bug 1085382. Using the toolkit safebrowsing is bug 778611. + // + // Because these static link checks tend to cause false positives + // we delay showing the warning until a user tries to click the link. + if (failsStaticTests) { + return true; + } + } + + return false; + } + + /** + * Opens the default browser to a page where the user can submit the given url + * as a phish. + * + * @param aPhishingURL the url we want to report back as a phishing attack + */ + reportPhishingURL(aPhishingURL) { + let reportUrl = Services.urlFormatter.formatURLPref( + "browser.safebrowsing.reportPhishURL" + ); + reportUrl += "&url=" + encodeURIComponent(aPhishingURL); + + let uri = Services.io.newURI(reportUrl); + let protocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + protocolSvc.loadURI(uri); + } + + /** + * Private helper method to determine if the link node contains a user visible + * url with a host name that differs from the actual href the user would get + * taken to. + * i.e. <a href="http://myevilsite.com">http://mozilla.org</a> + * + * @returns true if aHrefURL.host does NOT match the host of the link node text + */ + misMatchedHostWithLinkText(aHrefURL, aLinkNodeText) { + // gatherTextUnder puts a space between each piece of text it gathers, + // so strip the spaces out (see bug 326082 for details). + aLinkNodeText = aLinkNodeText.replace(/ /g, ""); + + // Only worry about http: and https: urls. + if (/^https?:/.test(aLinkNodeText)) { + let linkTextURI = Services.io.newURI(aLinkNodeText); + + // Compare the base domain of the href and the link text. + try { + return ( + Services.eTLD.getBaseDomain(aHrefURL) != + Services.eTLD.getBaseDomain(linkTextURI) + ); + } catch (e) { + // If we throw above, one of the URIs probably has no TLD (e.g. + // http://localhost), so just check the entire host. + return aHrefURL.host != linkTextURI.host; + } + } + + return false; + } + + /** + * If the current message has been identified as an email scam, prompts the + * user with a warning before allowing the link click to be processed. + * The warning prompt includes the unobscured host name of the http(s) url the + * user clicked on. + * + * @param {DOMWindow} win + * The window the message is being displayed within. + * @param {string} aUrl + * The url of the message + * @param {string} [aLinkText] + * User visible link text associated with the link + * @returns {number} + * 0 if the URL implied by aLinkText should be used instead. + * 1 if the request should be blocked. + * 2 if aUrl should be allowed to load. + */ + warnOnSuspiciousLinkClick(win, aUrl, aLinkText) { + if (!this.#analyzeUrl(aUrl, aLinkText)) { + return 2; // No problem with the url. Allow it to load. + } + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + + // Analysis said there was a problem. + if (aLinkText && /^https?:/i.test(aLinkText)) { + let actualURI = Services.io.newURI(aUrl); + let displayedURI; + try { + displayedURI = Services.io.newURI(aLinkText); + } catch (e) { + return 1; + } + + let titleMsg = bundle.GetStringFromName("linkMismatchTitle"); + let dialogMsg = bundle.formatStringFromName( + "confirmPhishingUrlAlternate", + [displayedURI.host, actualURI.host] + ); + let warningButtons = + Ci.nsIPromptService.BUTTON_POS_0 * + Ci.nsIPromptService.BUTTON_TITLE_IS_STRING + + Ci.nsIPromptService.BUTTON_POS_1 * + Ci.nsIPromptService.BUTTON_TITLE_CANCEL + + Ci.nsIPromptService.BUTTON_POS_2 * + Ci.nsIPromptService.BUTTON_TITLE_IS_STRING; + let button0Text = bundle.formatStringFromName("confirmPhishingGoDirect", [ + displayedURI.host, + ]); + let button2Text = bundle.formatStringFromName("confirmPhishingGoAhead", [ + actualURI.host, + ]); + return Services.prompt.confirmEx( + win, + titleMsg, + dialogMsg, + warningButtons, + button0Text, + "", + button2Text, + "", + {} + ); + } + + let hrefURL; + try { + // make sure relative link urls don't make us bail out + hrefURL = Services.io.newURI(aUrl); + } catch (e) { + return 1; // block the load + } + + // only prompt for http and https urls + if (hrefURL.schemeIs("http") || hrefURL.schemeIs("https")) { + // unobscure the host name in case it's an encoded ip address.. + let unobscuredHostNameValue = + lazy.isLegalIPAddress(hrefURL.host, true) || hrefURL.host; + + let brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + let brandShortName = brandBundle.GetStringFromName("brandShortName"); + let titleMsg = bundle.GetStringFromName("confirmPhishingTitle"); + let dialogMsg = bundle.formatStringFromName("confirmPhishingUrl", [ + brandShortName, + unobscuredHostNameValue, + ]); + let warningButtons = + Ci.nsIPromptService.STD_YES_NO_BUTTONS + + Ci.nsIPromptService.BUTTON_POS_1_DEFAULT; + let button = Services.prompt.confirmEx( + win, + titleMsg, + dialogMsg, + warningButtons, + "", + "", + "", + "", + {} + ); + return button == 0 ? 2 : 1; // 2 == allow, 1 == block + } + return 2; // allow the link to load + } +})(); |