# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- # ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Thunderbird Phishing Dectector # # The Initial Developer of the Original Code is # The Mozilla Foundation. # Portions created by the Initial Developer are Copyright (C) 2005 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Scott MacGregor # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ****** // Dependencies: // gPrefBranch, gBrandBundle, gMessengerBundle should already be defined // gatherTextUnder from utilityOverlay.js const kPhishingNotSuspicious = 0; const kPhishingWithIPAddress = 1; const kPhishingWithMismatchedHosts = 2; ////////////////////////////////////////////////////////////////////////////// // isEmailScam --> examines the message currently loaded in the message pane // and returns true if we think that message is an e-mail scam. // Assumes the message has been completely loaded in the message pane (i.e. OnMsgParsed has fired) // aUrl: nsIURI object for the msg we want to examine... ////////////////////////////////////////////////////////////////////////////// function isMsgEmailScam(aUrl) { var isEmailScam = false; if (!aUrl || !gPrefBranch.getBoolPref("mail.phishing.detection.enabled")) return isEmailScam; // Ignore nntp and RSS messages var folder = aUrl.folder; if (folder.server.type == 'nntp' || folder.server.type == 'rss') return isEmailScam; // loop through all of the link nodes in the message's DOM, looking for phishing URLs... var msgDocument = document.getElementById('messagepane').contentDocument; // examine all links... var linkNodes = msgDocument.links; for (var index = 0; index < linkNodes.length && !isEmailScam; index++) isEmailScam = isPhishingURL(linkNodes[index], true); // if an e-mail contains a form element, then assume the message is a phishing attack. // Legitimate sites should not be using forms inside of e-mail. if (!isEmailScam && msgDocument.getElementsByTagName("form").length > 0) isEmailScam = true; // we'll add more checks here as our detector matures.... return isEmailScam; } ////////////////////////////////////////////////////////////////////////////// // isPhishingURL --> examines the passed in linkNode and returns true if we think // the URL is an email scam. // aLinkNode: the link node to examine // aSilentMode: don't prompt the user to confirm ////////////////////////////////////////////////////////////////////////////// function isPhishingURL(aLinkNode, aSilentMode) { if (!gPrefBranch.getBoolPref("mail.phishing.detection.enabled")) return false; var phishingType = kPhishingNotSuspicious; var href = aLinkNode.href; if (!href) return false; var linkTextURL = {}; var unobscuredHostName = {}; var isPhishingURL = false; var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); var hrefURL; // make sure relative link urls don't make us bail out try { hrefURL = ioService.newURI(href, null, null); } 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')) { unobscuredHostName.value = hrefURL.host; if (hostNameIsIPAddress(hrefURL.host, unobscuredHostName) && !isLocalIPAddress(unobscuredHostName)) phishingType = kPhishingWithIPAddress; else if (misMatchedHostWithLinkText(aLinkNode, hrefURL, linkTextURL)) phishingType = kPhishingWithMismatchedHosts; isPhishingURL = phishingType != kPhishingNotSuspicious; if (!aSilentMode && isPhishingURL) // allow the user to override the decision isPhishingURL = confirmSuspiciousURL(phishingType, unobscuredHostName.value); } return isPhishingURL; } ////////////////////////////////////////////////////////////////////////////// // helper methods in support of isPhishingURL ////////////////////////////////////////////////////////////////////////////// function misMatchedHostWithLinkText(aLinkNode, aHrefURL, aLinkTextURL) { var linkNodeText = gatherTextUnder(aLinkNode); // gatherTextUnder puts a space between each piece of text it gathers, // so strip the spaces out (see bug 326082 for details). linkNodeText = linkNodeText.replace(/ /g, ""); // only worry about http and https urls if (linkNodeText) { // does the link text look like a http url? if (linkNodeText.search(/(^http:|^https:)/) != -1) { var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); var linkTextURL = ioService.newURI(linkNodeText, null, null); aLinkTextURL.value = linkTextURL; return aHrefURL.host != linkTextURL.host; } } return false; } // returns true if the hostName is an IP address // if the host name is an obscured IP address, returns the unobscured host function hostNameIsIPAddress(aHostName, aUnobscuredHostName) { // TODO: Add Support for IPv6 var index; // scammers frequently obscure the IP address by encoding each component as octal, hex // or in some cases a mix match of each. The IP address could also be represented as a DWORD. // break the IP address down into individual components. var ipComponents = aHostName.split("."); // if we didn't find at least 4 parts to our IP address it either isn't a numerical IP // or it is encoded as a dword if (ipComponents.length < 4) { // Convert to a binary to test for possible DWORD. var binaryDword = parseInt(aHostName).toString(2); if (isNaN(binaryDword)) return false; // convert the dword into its component IP parts. ipComponents = new Array; ipComponents[0] = (aHostName >> 24) & 255; ipComponents[1] = (aHostName >> 16) & 255; ipComponents[2] = (aHostName >> 8) & 255; ipComponents[3] = (aHostName & 255); } else { for (index = 0; index < ipComponents.length; ++index) { // by leaving the radix parameter blank, we can handle IP addresses // where one component is hex, another is octal, etc. ipComponents[index] = parseInt(ipComponents[index]); } } // make sure each part of the IP address is in fact a number for (index = 0; index < ipComponents.length; ++index) if (isNaN(ipComponents[index])) // if any part of the IP address is not a number, then we can safely return return false; var hostName = ipComponents[0] + '.' + ipComponents[1] + '.' + ipComponents[2] + '.' + ipComponents[3]; // only set aUnobscuredHostName if we are looking at an IPv4 host name if (isIPv4HostName(hostName)) { aUnobscuredHostName.value = hostName; return true; } return false; } function isIPv4HostName(aHostName) { var ipv4HostRegExp = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/); // IPv4 // treat 0.0.0.0 as an invalid IP address return ipv4HostRegExp.test(aHostName) && aHostName != '0.0.0.0'; } // returns true if the user confirms the URL is a scam function confirmSuspiciousURL(aPhishingType, aSuspiciousHostName) { var brandShortName = gBrandBundle.getString("brandShortName"); var titleMsg = gMessengerBundle.getString("confirmPhishingTitle"); var dialogMsg; switch (aPhishingType) { case kPhishingWithIPAddress: case kPhishingWithMismatchedHosts: dialogMsg = gMessengerBundle.getFormattedString("confirmPhishingUrl" + aPhishingType, [brandShortName, aSuspiciousHostName], 2); break; default: return false; } const nsIPS = Components.interfaces.nsIPromptService; var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(nsIPS); var buttons = nsIPS.STD_YES_NO_BUTTONS + nsIPS.BUTTON_POS_1_DEFAULT; return promptService.confirmEx(window, titleMsg, dialogMsg, buttons, "", "", "", "", {}); /* the yes button is in position 0 */ } // returns true if the IP address is a local address. function isLocalIPAddress(unobscuredHostName) { var ipComponents = unobscuredHostName.value.split("."); return ipComponents[0] == 10 || (ipComponents[0] == 192 && ipComponents[1] == 168) || (ipComponents[0] == 169 && ipComponents[1] == 254) || (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32); }