summaryrefslogtreecommitdiffstats
path: root/comm/suite/mailnews/content/phishingDetector.js
blob: 04d291075357f90f85025e175119c1348090a31a (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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */

// Dependencies:
// gBrandBundle, gMessengerBundle should already be defined
// gatherTextUnder from utilityOverlay.js

ChromeUtils.import("resource:///modules/hostnameUtils.jsm");

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 || !Services.prefs.getBoolPref("mail.phishing.detection.enabled"))
    return isEmailScam;

  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.server.type == 'nntp' || folder.server.type == 'rss')
      return isEmailScam;

    // 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 isEmailScam;

  } catch (ex) {
    if (ex.result != Cr.NS_ERROR_FAILURE)
      throw ex;
  }

  // loop through all of the link nodes in the message's DOM, looking for phishing URLs...
  var msgDocument = document.getElementById('messagepane').contentDocument;
  var index;

  // examine all links...
  var linkNodes = msgDocument.links;
  for (index = 0; index < linkNodes.length && !isEmailScam; index++)
    isEmailScam = isPhishingURL(linkNodes[index], true);

  // if an e-mail contains a non-addressbook form element, then assume the message is
  // a phishing attack. Legitimate sites should not be using forms inside of e-mail
  if (!isEmailScam)
  {
    var forms = msgDocument.getElementsByTagName("form");
    for (index = 0; index < forms.length && !isEmailScam; index++)
      isEmailScam = forms[index].action != "" && !/^addbook:/.test(forms[index].action);
  }

  // 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
// aHref: optional href for XLinks
//////////////////////////////////////////////////////////////////////////////

function isPhishingURL(aLinkNode, aSilentMode, aHref)
{
  if (!Services.prefs.getBoolPref("mail.phishing.detection.enabled"))
    return false;

  var phishingType = kPhishingNotSuspicious;
  var aLinkText = gatherTextUnder(aLinkNode);
  var href = aHref || aLinkNode.href;
  if (!href)
    return false;

  var linkTextURL = {};
  var isPhishingURL = false;

  var hrefURL;
  // Make sure relative link urls don't make us bail out.
  try {
    hrefURL = Services.io.newURI(href);
  } 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'))
  {

    if (aLinkText)
      aLinkText = aLinkText.replace(/^<(.+)>$|^"(.+)"$/, "$1$2");
    if (aLinkText != aLinkNode.href &&
        aLinkText.replace(/\/+$/, "") != aLinkNode.href.replace(/\/+$/, ""))
    {
      let ipAddress = isLegalIPAddress(hrefURL.host, true);
      if (ipAddress && !isLegalLocalIPAddress(ipAddress))
        phishingType = kPhishingWithIPAddress;
      else if (misMatchedHostWithLinkText(aLinkNode, hrefURL))
        phishingType = kPhishingWithMismatchedHosts;

      isPhishingURL = phishingType != kPhishingNotSuspicious;

      if (!aSilentMode && isPhishingURL) // allow the user to override the decision
        isPhishingURL = confirmSuspiciousURL(phishingType, hrefURL.host);
    }
  }

  return isPhishingURL;
}

//////////////////////////////////////////////////////////////////////////////
// helper methods in support of isPhishingURL
//////////////////////////////////////////////////////////////////////////////

function misMatchedHostWithLinkText(aLinkNode, aHrefURL)
{
  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 linkURI  = Services.io.newURI(linkNodeText);
       // compare hosts, but ignore possible www. prefix
       return !(aHrefURL.host.replace(/^www\./, "") == linkURI.host.replace(/^www\./, ""));
     }
  }

  return false;
}

// 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;
  }

  var buttons = Services.prompt.STD_YES_NO_BUTTONS +
                Services.prompt.BUTTON_POS_1_DEFAULT;
  return Services.prompt.confirmEx(window, titleMsg, dialogMsg, buttons, "", "", "", "", {}); /* the yes button is in position 0 */
}