!import
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1 //@line 38 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/mail/base/content/phishingDetector.js"
2
3 // Dependencies:
4 // gPrefBranch, gBrandBundle, gMessengerBundle should already be defined
5 // gatherTextUnder from utilityOverlay.js
6
7 const kPhishingNotSuspicious = 0;
8 const kPhishingWithIPAddress = 1;
9 const kPhishingWithMismatchedHosts = 2;
10
11 var gPhishingDetector = {
12 mCheckForIPAddresses: true,
13 mCheckForMismatchedHosts: true,
14 mPhishingWarden: null,
15
shutdown
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
16 shutdown: function()
17 {
18 try {
19 this.mPhishingWarden.shutdown();
20 } catch (ex) {}
21 },
22
23 /**
24 * initialize the phishing warden.
25 * initialize the black and white list url tables.
26 * update the local tables if necessary
27 */
init
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
28 init: function()
29 {
30 try {
31 // set up the anti phishing service
32 var appContext = Components.classes["@mozilla.org/phishingprotection/application;1"]
33 .getService().wrappedJSObject;
34
35 this.mPhishingWarden = new appContext.PROT_PhishingWarden();
36
37 // Register tables
38 // XXX: move table names to a pref that we originally will download
39 // from the provider (need to workout protocol details)
40 this.mPhishingWarden.registerWhiteTable("goog-white-exp");
41 this.mPhishingWarden.registerBlackTable("goog-phish-sha128");
42
43 // Download/update lists if we're in non-enhanced mode
44 this.mPhishingWarden.maybeToggleUpdateChecking();
45 } catch (ex) { dump('unable to create the phishing warden: ' + ex + '\n');}
46
47 this.mCheckForIPAddresses = gPrefBranch.getBoolPref("mail.phishing.detection.ipaddresses");
48 this.mCheckForMismatchedHosts = gPrefBranch.getBoolPref("mail.phishing.detection.mismatched_hosts");
49 },
50
51 /**
52 * Analyzes the urls contained in the currently loaded message in the message pane, looking for
53 * phishing URLs.
54 * Assumes the message has finished loading in the message pane (i.e. OnMsgParsed has fired).
55 *
56 * @param aUrl nsIURI for the message being analyzed.
57 *
58 * @return asynchronously calls gMessageNotificationBar.setPhishingMsg if the message
59 * is identified as a scam.
60 */
analyzeMsgForPhishingURLs
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
61 analyzeMsgForPhishingURLs: function (aUrl)
62 {
63 if (!aUrl || !gPrefBranch.getBoolPref("mail.phishing.detection.enabled"))
64 return;
65
66 // Ignore nntp and RSS messages
67 var folder;
68 try {
69 folder = aUrl.folder;
70 if (folder.server.type == 'nntp' || folder.server.type == 'rss')
71 return;
72 } catch (ex) {}
73
74 // extract the link nodes in the message and analyze them, looking for suspicious URLs...
75 var linkNodes = document.getElementById('messagepane').contentDocument.links;
76 for (var index = 0; index < linkNodes.length; index++)
77 this.analyzeUrl(linkNodes[index].href, gatherTextUnder(linkNodes[index]));
78
79 // extract the action urls associated with any form elements in the message and analyze them.
80 var formNodes = document.getElementById('messagepane').contentDocument.getElementsByTagName("form");
81 for (index = 0; index < formNodes.length; index++)
82 {
83 if (formNodes[index].action)
84 this.analyzeUrl(formNodes[index].action);
85 }
86 },
87
88 /**
89 * Analyze the url contained in aLinkNode for phishing attacks. If a phishing URL is found,
90 *
91 * @param aHref the url to be analyzed
92 * @param aLinkText (optional) user visible link text associated with aHref in case
93 * we are dealing with a link node.
94 * @return asynchronously calls gMessageNotificationBar.setPhishingMsg if the link node
95 * contains a phishing URL.
96 */
analyzeUrl
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
97 analyzeUrl: function (aUrl, aLinkText)
98 {
99 if (!aUrl)
100 return;
101
102 var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
103 var hrefURL;
104 // make sure relative link urls don't make us bail out
105 try {
106 hrefURL = ioService.newURI(aUrl, null, null);
107 } catch(ex) { return; }
108
109 // only check for phishing urls if the url is an http or https link.
110 // this prevents us from flagging imap and other internally handled urls
111 if (hrefURL.schemeIs('http') || hrefURL.schemeIs('https'))
112 {
113 var linkTextURL = {};
114 var unobscuredHostName = {};
115 unobscuredHostName.value = hrefURL.host;
116
117 // The link is not suspicious if the visible text is the same as the URL,
118 // even if the URL is an IP address. URLs are commonly surrounded by
119 // < > or "" (RFC2396E) - so strip those from the link text before comparing.
120 if (aLinkText)
121 aLinkText = aLinkText.replace(/^<(.+)>$|^"(.+)"$/, "$1$2");
122
123 var failsStaticTests = (aLinkText != aUrl) &&
124 ((this.mCheckForIPAddresses && this.hostNameIsIPAddress(hrefURL.host, unobscuredHostName) &&
125 !this.isLocalIPAddress(unobscuredHostName)) ||
126 (this.mCheckForMismatchedHosts && aLinkText &&
127 this.misMatchedHostWithLinkText(hrefURL, aLinkText, linkTextURL)));
128
129 // Lookup the url against our local list. We want to do this even if the url fails our static
130 // test checks because the url might be in the white list.
131 if (this.mPhishingWarden)
132 this.mPhishingWarden.isEvilURL(GetLoadedMessage(), failsStaticTests, aUrl, this.localListCallback);
133 else
134 this.localListCallback(GetLoadedMessage(), failsStaticTests, aUrl, 2 /* not found */);
135 }
136 },
137
138 /**
139 *
140 * @param aMsgURI the uri for the loaded message when the look up was initiated.
141 * @param aFailsStaticTests true if our static tests think the url is a phishing scam
142 * @param aUrl the url we looked up in the phishing tables
143 * @param aLocalListStatus the result of the local lookup (PROT_ListWarden.IN_BLACKLIST,
144 * PROT_ListWarden.IN_WHITELIST or PROT_ListWarden.NOT_FOUND.
145 */
localListCallback
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
146 localListCallback: function (aMsgURI, aFailsStaticTests, aUrl, aLocalListStatus)
147 {
148 // for urls in the blacklist, notify the phishing bar.
149 // for urls in the whitelist, do nothing
150 // for all other urls, fall back to the static tests
151 if (aMsgURI == GetLoadedMessage())
152 {
153 if (aLocalListStatus == 0 /* PROT_ListWarden.IN_BLACKLIST */ ||
154 (aLocalListStatus == 2 /* PROT_ListWarden.PROT_ListWarden.NOT_FOUND */ && aFailsStaticTests))
155 gMessageNotificationBar.setPhishingMsg();
156 }
157 },
158
159 /**
160 * Looks up the report phishing url for the current phishing provider, appends aPhishingURL to the url,
161 * and loads it in the default browser where the user can submit the url as a phish.
162 * @param aPhishingURL the url we want to report back as a phishing attack
163 */
reportPhishingURL
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
164 reportPhishingURL: function(aPhishingURL)
165 {
166 var appContext = Components.classes["@mozilla.org/phishingprotection/application;1"]
167 .getService().wrappedJSObject;
168 var reportUrl = appContext.getReportPhishingURL();
169 if (reportUrl)
170 {
171 reportUrl += "&url=" + encodeURIComponent(aPhishingURL);
172 // now send the url to the default browser
173
174 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
175 .getService(Components.interfaces.nsIIOService);
176 var uri = ioService.newURI(reportUrl, null, null);
177 var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
178 .getService(Components.interfaces.nsIExternalProtocolService);
179 protocolSvc.loadUrl(uri);
180 }
181 },
182
183 /**
184 * Private helper method to determine if the link node contains a user visible
185 * url with a host name that differs from the actual href the user would get taken to.
186 * i.e. <a href="http://myevilsite.com">http://mozilla.org</a>
187 *
188 * @return true if aHrefURL.host matches the host of the link node text.
189 * @return aLinkTextURL the nsIURI for the link node text
190 */
misMatchedHostWithLinkText
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
191 misMatchedHostWithLinkText: function(aHrefURL, aLinkNodeText, aLinkTextURL)
192 {
193 // gatherTextUnder puts a space between each piece of text it gathers,
194 // so strip the spaces out (see bug 326082 for details).
195 aLinkNodeText = aLinkNodeText.replace(/ /g, "");
196
197 // only worry about http and https urls
198 if (aLinkNodeText)
199 {
200 // does the link text look like a http url?
201 if (aLinkNodeText.search(/(^http:|^https:)/) != -1)
202 {
203 var ioService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
204 aLinkTextURL.value = ioService.newURI(aLinkNodeText, null, null);
205 // compare hosts, but ignore possible www. prefix
206 return !(aHrefURL.host.replace(/^www\./, "") == aLinkTextURL.value.host.replace(/^www\./, ""));
207 }
208 }
209
210 return false;
211 },
212
213 /**
214 * Private helper method to determine if aHostName is an obscured IP address
215 * @return unobscured host name (if there is one)
216 * @return true if aHostName is an IP address
217 */
hostNameIsIPAddress
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
218 hostNameIsIPAddress: function(aHostName, aUnobscuredHostName)
219 {
220 // TODO: Add Support for IPv6
221 var index;
222
223 // scammers frequently obscure the IP address by encoding each component as octal, hex
224 // or in some cases a mix match of each. The IP address could also be represented as a DWORD.
225
226 // break the IP address down into individual components.
227 var ipComponents = aHostName.split(".");
228
229 // if we didn't find at least 4 parts to our IP address it either isn't a numerical IP
230 // or it is encoded as a dword
231 if (ipComponents.length < 4)
232 {
233 // Convert to a binary to test for possible DWORD.
234 var binaryDword = parseInt(aHostName).toString(2);
235 if (isNaN(binaryDword))
236 return false;
237
238 // convert the dword into its component IP parts.
239 ipComponents = new Array;
240 ipComponents[0] = (aHostName >> 24) & 255;
241 ipComponents[1] = (aHostName >> 16) & 255;
242 ipComponents[2] = (aHostName >> 8) & 255;
243 ipComponents[3] = (aHostName & 255);
244 }
245 else
246 {
247 for (index = 0; index < ipComponents.length; ++index)
248 {
249 // by leaving the radix parameter blank, we can handle IP addresses
250 // where one component is hex, another is octal, etc.
251 ipComponents[index] = parseInt(ipComponents[index]);
252 }
253 }
254
255 // make sure each part of the IP address is in fact a number
256 for (index = 0; index < ipComponents.length; ++index)
257 if (isNaN(ipComponents[index])) // if any part of the IP address is not a number, then we can safely return
258 return false;
259
260 var hostName = ipComponents[0] + '.' + ipComponents[1] + '.' + ipComponents[2] + '.' + ipComponents[3];
261
262 // only set aUnobscuredHostName if we are looking at an IPv4 host name
263 if (this.isIPv4HostName(hostName))
264 {
265 aUnobscuredHostName.value = hostName;
266 return true;
267 }
268 return false;
269 },
270
271 /**
272 * Private helper method.
273 * @return true if aHostName is an IPv4 address
274 */
isIPv4HostName
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
275 isIPv4HostName: function(aHostName)
276 {
277 var ipv4HostRegExp = new RegExp(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/); // IPv4
278 // treat 0.0.0.0 as an invalid IP address
279 return ipv4HostRegExp.test(aHostName) && aHostName != '0.0.0.0';
280 },
281
282 /**
283 * Private helper method.
284 * @return true if unobscuredHostName is a local IP address.
285 */
isLocalIPAddress
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
286 isLocalIPAddress: function(unobscuredHostName)
287 {
288 var ipComponents = unobscuredHostName.value.split(".");
289
290 return ipComponents[0] == 10 ||
291 ipComponents[0] == 127 || // loopback address
292 (ipComponents[0] == 192 && ipComponents[1] == 168) ||
293 (ipComponents[0] == 169 && ipComponents[1] == 254) ||
294 (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32);
295 },
296
297 /**
298 * If the current message has been identified as an email scam, prompts the user with a warning
299 * before allowing the link click to be processed. The warning prompt includes the unobscured host name
300 * of the http(s) url the user clicked on.
301 *
302 * @param aUrl the url
303 * @return true if the link should be allowed to load
304 */
warnOnSuspiciousLinkClick
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
305 warnOnSuspiciousLinkClick: function(aUrl)
306 {
307 // if the loaded message has been flagged as a phishing scam,
308 if (!gMessageNotificationBar.isFlagSet(kMsgNotificationPhishingBar))
309 return true;
310
311 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
312 .getService(Components.interfaces.nsIIOService);
313 var hrefURL;
314 // make sure relative link urls don't make us bail out
315 try {
316 hrefURL = ioService.newURI(aUrl, null, null);
317 } catch(ex) { return false; }
318
319 // only prompt for http and https urls
320 if (hrefURL.schemeIs('http') || hrefURL.schemeIs('https'))
321 {
322 // unobscure the host name in case it's an encoded ip address..
323 var unobscuredHostName = {};
324 unobscuredHostName.value = hrefURL.host;
325 this.hostNameIsIPAddress(hrefURL.host, unobscuredHostName);
326
327 var brandShortName = gBrandBundle.getString("brandShortName");
328 var titleMsg = gMessengerBundle.getString("confirmPhishingTitle");
329 var dialogMsg = gMessengerBundle.getFormattedString("confirmPhishingUrl",
330 [brandShortName, unobscuredHostName.value], 2);
331
332 const nsIPS = Components.interfaces.nsIPromptService;
333 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(nsIPS);
334 return !promptService.confirmEx(window, titleMsg, dialogMsg, nsIPS.STD_YES_NO_BUTTONS + nsIPS.BUTTON_POS_1_DEFAULT,
335 "", "", "", "", {}); /* the yes button is in position 0 */
336 }
337
338 return true; // allow the link to load
339 }
340 };