Bug 1407878 - Check URLs against the login reputation service when a password field is focused. r?francois, MattN
MozReview-Commit-ID: 8csNJU5hK7h
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -152,16 +152,17 @@ const listeners = {
"Reader:UpdateReaderButton": ["ReaderParent"],
// PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
"RemoteLogins:findLogins": ["LoginManagerParent"],
"RemoteLogins:findRecipes": ["LoginManagerParent"],
"RemoteLogins:onFormSubmit": ["LoginManagerParent"],
"RemoteLogins:autoCompleteLogins": ["LoginManagerParent"],
"RemoteLogins:removeLogin": ["LoginManagerParent"],
"RemoteLogins:insecureLoginFormPresent": ["LoginManagerParent"],
+ "RemoteLogins:queryLoginReputation": ["LoginManagerParent"],
// PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
"WCCR:registerProtocolHandler": ["Feeds"],
"WCCR:registerContentHandler": ["Feeds"],
"rtcpeer:CancelRequest": ["webrtcUI"],
"rtcpeer:Request": ["webrtcUI"],
"webrtc:CancelRequest": ["webrtcUI"],
"webrtc:Request": ["webrtcUI"],
"webrtc:StopRecording": ["webrtcUI"],
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -87,16 +87,17 @@ BrowserCLH.prototype = {
mm: [
// PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN nsBrowserGlue.js
"RemoteLogins:findLogins",
"RemoteLogins:findRecipes",
"RemoteLogins:onFormSubmit",
"RemoteLogins:autoCompleteLogins",
"RemoteLogins:removeLogin",
"RemoteLogins:insecureLoginFormPresent",
+ "RemoteLogins:queryLoginReputation",
// PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN nsBrowserGlue.js
],
});
GeckoViewUtils.addLazyGetter(this, "LoginManagerContent", {
module: "resource://gre/modules/LoginManagerContent.jsm",
});
GeckoViewUtils.addLazyGetter(this, "ActionBarHandler", {
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -32,16 +32,17 @@ this.LoginHelper = {
/**
* Warning: these only update if a logger was created.
*/
debug: Services.prefs.getBoolPref("signon.debug"),
formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
showInsecureFieldWarning: Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled"),
+ loginReputationEnabled: Services.prefs.getBoolPref("browser.safebrowsing.passwords.enabled"),
createLogger(aLogPrefix) {
let getMaxLogLevel = () => {
return this.debug ? "debug" : "warn";
};
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
@@ -59,16 +60,20 @@ this.LoginHelper = {
this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
logger.maxLogLevel = getMaxLogLevel();
});
Services.prefs.addObserver("security.insecure_field_warning.", () => {
this.showInsecureFieldWarning = Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled");
});
+ Services.prefs.addObserver("browser.safebrowsing.passwords.enabled", () => {
+ this.loginReputationEnabled = Services.prefs.getBoolPref("browser.safebrowsing.passwords.enabled");
+ });
+
return logger;
},
/**
* Due to the way the signons2.txt file is formatted, we need to make
* sure certain field values or characters do not cause the file to
* be parsed incorrectly. Reject hostnames that we can't store correctly.
*
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -176,17 +176,18 @@ var LoginManagerContent = {
},
_getRandomId() {
return Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator).generateUUID().toString();
},
_messages: [ "RemoteLogins:loginsFound",
- "RemoteLogins:loginsAutoCompleted" ],
+ "RemoteLogins:loginsAutoCompleted",
+ "RemoteLogins:queryReputationCompleted"],
/**
* WeakMap of the root element of a FormLike to the FormLike representing its fields.
*
* This is used to be able to lookup an existing FormLike for a given root element since multiple
* calls to LoginFormFactory won't give the exact same object. When batching fills we don't always
* want to use the most recent list of elements for a FormLike since we may end up doing multiple
* fills for the same set of elements when a field gets added between arming and running the
@@ -281,16 +282,21 @@ var LoginManagerContent = {
case "RemoteLogins:loginsAutoCompleted": {
let loginsFound =
LoginHelper.vanillaObjectsToLogins(msg.data.logins);
let messageManager = msg.target;
request.promise.resolve({ logins: loginsFound, messageManager });
break;
}
+
+ case "RemoteLogins:queryReputationCompleted": {
+ request.promise.resolve(msg.data.result);
+ break;
+ }
}
},
/**
* Get relevant logins and recipes from the parent
*
* @param {HTMLFormElement} form - form to get login data for
* @param {Object} options
@@ -345,16 +351,36 @@ var LoginManagerContent = {
isPasswordField: aElement.type == "password",
};
return this._sendRequest(messageManager, requestData,
"RemoteLogins:autoCompleteLogins",
messageData);
},
+ _queryLoginReputationAsync(aElement) {
+ let doc = aElement.ownerDocument;
+ let form = LoginFormFactory.createFromField(aElement);
+ let win = doc.defaultView;
+
+ let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
+ let actionOrigin = LoginUtils._getActionOrigin(form);
+
+ let messageManager = messageManagerFromWindow(win);
+
+ let requestData = {};
+ let messageData = { formOrigin,
+ actionOrigin,
+ };
+
+ return this._sendRequest(messageManager, requestData,
+ "RemoteLogins:queryLoginReputation",
+ messageData);
+ },
+
setupProgressListener(window) {
if (!LoginHelper.formlessCaptureEnabled) {
return;
}
try {
let webProgress = window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -108,16 +108,24 @@ var LoginManagerParent = {
break;
}
case "RemoteLogins:removeLogin": {
let login = LoginHelper.vanillaObjectToLogin(data.login);
AutoCompletePopup.removeLogin(login);
break;
}
+
+ case "RemoteLogins:queryLoginReputation": {
+ this.doQueryLoginReputation(data.formOrigin,
+ data.actionOrigin,
+ data.requestId,
+ msg.target);
+ break;
+ }
}
return undefined;
},
/**
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
* to the child process (LoginManagerContent).
@@ -282,16 +290,39 @@ var LoginManagerParent = {
// doesn't support structured cloning.
var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
requestId,
logins: jsLogins,
});
},
+ doQueryLoginReputation(formOrigin, actionOrigin, requestId, target) {
+ let service = Cc["@mozilla.org/reputationservice/login-reputation-service;1"].
+ getService(Ci.ILoginReputationService);
+
+ let param = {
+ formOrigin,
+ actionOrigin,
+ };
+
+ service.queryReputation(param, {
+ onQueryComplete(aResult) {
+ if (!target.messageManager) {
+ // It is possible that child process is removed when the query completed.
+ return;
+ }
+ target.messageManager.sendAsyncMessage("RemoteLogins:queryReputationCompleted", {
+ requestId,
+ result: aResult
+ });
+ }
+ });
+ },
+
onFormSubmit(hostname, formSubmitURL,
usernameField, newPasswordField,
oldPasswordField, openerTopWindow,
target) {
function getPrompter() {
var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
createInstance(Ci.nsILoginManagerPrompter);
prompterSvc.init(target.ownerGlobal);
--- a/toolkit/components/passwordmgr/nsILoginManager.idl
+++ b/toolkit/components/passwordmgr/nsILoginManager.idl
@@ -7,16 +7,17 @@
interface nsIURI;
interface nsILoginInfo;
interface nsIAutoCompleteResult;
interface nsIFormAutoCompleteObserver;
interface nsIDOMHTMLInputElement;
interface nsIDOMHTMLFormElement;
interface nsIPropertyBag;
+interface IQueryReputationObserver;
[scriptable, uuid(38c7f6af-7df9-49c7-b558-2776b24e6cc1)]
interface nsILoginManager : nsISupports {
/**
* This promise is resolved when initialization is complete, and is rejected
* in case initialization failed. This includes the initial loading of the
* login data as well as any migration from previous versions.
*
@@ -216,16 +217,26 @@ interface nsILoginManager : nsISupports
in nsIDOMHTMLInputElement aElement,
in nsIFormAutoCompleteObserver aListener);
/**
* Stop a previously-started async search.
*/
void stopSearch();
+
+ /**
+ * Query the login reputation service for the URLs associated with a password field.
+ *
+ * NOTE: This is only enabled when 'browser.safebrowsing.passwords.enabled'
+ * preference is on.
+ */
+ void queryLoginReputationAsync(in nsIDOMHTMLInputElement aElement,
+ in IQueryReputationObserver aListener);
+
/**
* Search for logins in the login manager. An array is always returned;
* if there are no logins the array is empty.
*
* @param count
* The number of elements in the array. JS callers can simply use
* the array's .length property, and supply an dummy object for
* this out param. For example: |searchLogins({}, matchData)|
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -87,16 +87,20 @@ LoginManager.prototype = {
// Preferences. Add observer so we get notified of changes.
this._prefBranch = Services.prefs.getBranch("signon.");
this._prefBranch.addObserver("rememberSignons", this._observer);
this._remember = this._prefBranch.getBoolPref("rememberSignons");
this._autoCompleteLookupPromise = null;
+ // Cache ongoing queries to avoid requesting same query while one is
+ // still in flight.
+ this._queryLoginReputationElements = new Set();
+
// Form submit observer checks forms for new logins and pw changes.
Services.obs.addObserver(this._observer, "xpcom-shutdown");
if (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_DEFAULT) {
Services.obs.addObserver(this._observer, "passwordmgr-storage-replace");
// Initialize storage so that asynchronous data loading can start.
@@ -526,11 +530,45 @@ LoginManager.prototype = {
aElement, rect);
acLookupPromise.then(completeSearch.bind(this, acLookupPromise))
.catch(Cu.reportError);
},
stopSearch() {
this._autoCompleteLookupPromise = null;
},
+
+ queryLoginReputationAsync(aElement, aCallback) {
+ if (!LoginHelper.loginReputationEnabled) {
+ // Skip when login reputation is disabled.
+ return;
+ }
+
+ if (aElement.type !== "password") {
+ // Don't query login reputation when it's not a password field.
+ log.debug("QueryLoginReputation is not invoked because this isn't a password field");
+ return;
+ }
+
+ // We already have a request in flight for this field.
+ if (this._queryLoginReputationElements.has(aElement)) {
+ return;
+ }
+
+ this._queryLoginReputationElements.add(aElement);
+
+ LoginManagerContent._queryLoginReputationAsync(aElement)
+ .then((aResult) => {
+ log.debug("QueryLoginReputation invoked. Result is:" + aResult);
+ try {
+ this._queryLoginReputationElements.delete(aElement);
+ aCallback.onQueryReputationCompletion(aElement, aResult);
+ } catch (e) {
+ // Exception happens when the document is removed and we are still trying to
+ // access the input element. This may occur when page is navigated, for this
+ // case we should clear the cache.
+ this._queryLoginReputationElements.clear();
+ }
+ });
+ },
}; // end of LoginManager implementation
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);
--- a/toolkit/components/reputationservice/ILoginReputation.idl
+++ b/toolkit/components/reputationservice/ILoginReputation.idl
@@ -1,17 +1,39 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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/. */
#include "nsISupports.idl"
+[scriptable, uuid(6219f9da-297e-446d-8d47-ccdd8e72a1d5)]
+interface ILoginReputationResult : nsISupports {
+
+ // These should sync with 'VerdictType' defined in
+ // LoginReputationClientResponse in csd.proto.
+ const uint16_t UNSPECIFIED = 0;
+ const uint16_t SAFE = 1;
+ const uint16_t LOW_REPUTATION = 2;
+ const uint16_t PHISHING = 3;
+};
+
+[scriptable, uuid(c21ffe59-595f-46c8-9052-fefb639e196e)]
+interface ILoginReputationQuery : nsISupports {
+
+ readonly attribute ACString formOrigin;
+
+ readonly attribute ACString actionOrigin;
+};
+
[scriptable, uuid(b527be1e-8fbb-41d9-bee4-267a71236368)]
interface ILoginReputationQueryCallback : nsISupports {
- void onQueryComplete();
+ // aResult should be one of the const value defined in ILoginReputationResult
+ // interface.
+ void onQueryComplete(in uint16_t aResult);
};
[scriptable, uuid(1b3f1dfe-ce3a-486b-953e-ce5ac863eff9)]
interface ILoginReputationService : nsISupports {
- // XXX : Add QueryReputation interface
+ void queryReputation(in ILoginReputationQuery aQuery,
+ in ILoginReputationQueryCallback aCallback);
};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/reputationservice/IQueryReputationObserver.idl
@@ -0,0 +1,14 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMHTMLInputElement;
+
+[scriptable, function, uuid(d7e7ab50-13d4-4279-af2e-2762b172eb48)]
+interface IQueryReputationObserver : nsISupports
+{
+ void onQueryReputationCompletion(in nsIDOMHTMLInputElement aElement,
+ in uint16_t aResult);
+};
--- a/toolkit/components/reputationservice/LoginReputation.cpp
+++ b/toolkit/components/reputationservice/LoginReputation.cpp
@@ -31,8 +31,29 @@ LoginReputationService::LoginReputationS
{
LR_LOG(("Login reputation service starting up"));
}
LoginReputationService::~LoginReputationService()
{
LR_LOG(("Login reputation service shutting down"));
}
+
+NS_IMETHODIMP
+LoginReputationService::QueryReputation(ILoginReputationQuery* aQuery,
+ ILoginReputationQueryCallback* aCallback)
+{
+ NS_ENSURE_ARG_POINTER(aQuery);
+ NS_ENSURE_ARG_POINTER(aCallback);
+
+ if (LR_LOG_ENABLED()) {
+ nsAutoCString formOrigin, actionOrigin;
+ aQuery->GetFormOrigin(formOrigin);
+ aQuery->GetActionOrigin(actionOrigin);
+ LR_LOG(("Login reputation query parameters: formOrigin(%s), actionOrigin(%s)",
+ formOrigin.BeginReading(), actionOrigin.BeginReading()));
+ }
+
+ // Return SAFE until we add support for the remote service (bug 1413732).
+ aCallback->OnQueryComplete(ILoginReputationResult::SAFE);
+
+ return NS_OK;
+}
--- a/toolkit/components/reputationservice/moz.build
+++ b/toolkit/components/reputationservice/moz.build
@@ -6,16 +6,17 @@
with Files('*'):
BUG_COMPONENT = ('Toolkit', 'Safe Browsing')
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
XPIDL_SOURCES += [
'ILoginReputation.idl',
+ 'IQueryReputationObserver.idl',
'nsIApplicationReputation.idl',
]
XPIDL_MODULE = 'reputationservice'
UNIFIED_SOURCES += [
'ApplicationReputation.cpp',
'chromium/chrome/common/safe_browsing/csd.pb.cc',
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -78,16 +78,17 @@ NS_IMPL_CYCLE_COLLECTION(nsFormFillContr
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController)
NS_INTERFACE_MAP_ENTRY(nsIFormFillController)
NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput)
NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch)
NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY(IQueryReputationObserver)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFillController)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController)
nsFormFillController::nsFormFillController() :
@@ -892,16 +893,31 @@ nsFormFillController::StopSearch()
mLastFormAutoComplete->StopAutoCompleteSearch();
mLastFormAutoComplete = nullptr;
} else if (mLoginManager) {
mLoginManager->StopSearch();
}
return NS_OK;
}
+nsresult
+nsFormFillController::StartQueryLoginReputation(nsIDOMHTMLInputElement *aInput)
+{
+ if (!mLoginManager) {
+ mLoginManager = do_GetService("@mozilla.org/login-manager;1");
+ if (NS_WARN_IF(!mLoginManager)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ mLoginManager->QueryLoginReputationAsync(aInput, this);
+
+ return NS_OK;
+}
+
////////////////////////////////////////////////////////////////////////
//// nsIFormAutoCompleteObserver
NS_IMETHODIMP
nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult)
{
nsAutoString searchString;
aResult->GetSearchString(searchString);
@@ -911,16 +927,34 @@ nsFormFillController::OnSearchCompletion
if (mLastListener) {
mLastListener->OnSearchResult(this, aResult);
}
return NS_OK;
}
////////////////////////////////////////////////////////////////////////
+//// IQueryReputationObserver
+
+NS_IMETHODIMP
+nsFormFillController::OnQueryReputationCompletion(nsIDOMHTMLInputElement* aElement,
+ uint16_t aResult)
+{
+ MOZ_ASSERT(aElement);
+
+ MOZ_LOG(sLogger, LogLevel::Verbose, ("OnQueryReputationCompletion (%d)", aResult));
+
+ // TODO: integrate with browser UI (bug 1413389)
+ // Note that it is possible that the element is not visible to the user when we
+ // receive the callback (for example, page is navigated).
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+////////////////////////////////////////////////////////////////////////
//// nsIDOMEventListener
NS_IMETHODIMP
nsFormFillController::HandleEvent(nsIDOMEvent* aEvent)
{
WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
NS_ENSURE_STATE(internalEvent);
@@ -1070,16 +1104,22 @@ nsFormFillController::MaybeStartControll
bool isAutofillInput = false;
if (mAutofillInputs.Get(inputNode)) {
isAutofillInput = true;
}
if (isAutofillInput || isPwmgrInput || hasList || autocomplete) {
StartControllingInput(aInput);
}
+
+ // Trigger an asynchronous login reputation query when user focuses on the
+ // password field.
+ if (formControl->ControlType() == NS_FORM_INPUT_PASSWORD) {
+ StartQueryLoginReputation(aInput);
+ }
}
nsresult
nsFormFillController::Focus(nsIDOMEvent* aEvent)
{
nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(
aEvent->InternalDOMEvent()->GetTarget());
MaybeStartControllingInput(input);
--- a/toolkit/components/satchel/nsFormFillController.h
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -16,41 +16,44 @@
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
#include "nsIDocShell.h"
#include "nsIDOMHTMLInputElement.h"
#include "nsILoginManager.h"
#include "nsIMutationObserver.h"
#include "nsTArray.h"
#include "nsCycleCollectionParticipant.h"
+#include "IQueryReputationObserver.h"
// X.h defines KeyPress
#ifdef KeyPress
#undef KeyPress
#endif
class nsFormHistory;
class nsINode;
class nsPIDOMWindowOuter;
class nsFormFillController final : public nsIFormFillController,
public nsIAutoCompleteInput,
public nsIAutoCompleteSearch,
public nsIDOMEventListener,
public nsIFormAutoCompleteObserver,
- public nsIMutationObserver
+ public nsIMutationObserver,
+ public IQueryReputationObserver
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_NSIFORMFILLCONTROLLER
NS_DECL_NSIAUTOCOMPLETESEARCH
NS_DECL_NSIAUTOCOMPLETEINPUT
NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
NS_DECL_NSIDOMEVENTLISTENER
NS_DECL_NSIMUTATIONOBSERVER
+ NS_DECL_IQUERYREPUTATIONOBSERVER
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsFormFillController, nsIFormFillController)
nsresult Focus(nsIDOMEvent* aEvent);
nsresult KeyPress(nsIDOMEvent* aKeyEvent);
nsresult MouseDown(nsIDOMEvent* aMouseEvent);
nsFormFillController();
@@ -84,16 +87,18 @@ protected:
void MaybeRemoveMutationObserver(nsINode* aNode);
void RemoveForDocument(nsIDocument* aDoc);
bool IsEventTrusted(nsIDOMEvent *aEvent);
bool IsTextControl(nsINode* aNode);
+ nsresult StartQueryLoginReputation(nsIDOMHTMLInputElement *aInput);
+
// members //////////////////////////////////////////
nsCOMPtr<nsIAutoCompleteController> mController;
nsCOMPtr<nsILoginManager> mLoginManager;
nsIDOMHTMLInputElement* mFocusedInput;
nsINode* mFocusedInputNode;
// mListNode is a <datalist> element which, is set, has the form fill controller