The issue this is addresses is that [`CFURLCopyResourcePropertyForKey`](https://searchfox.org/mozilla-central/rev/ea7f70dac1c5fd18400f6d2a92679777d4b21492/xpcom/io/CocoaFileUtils.mm#212) does not return quarantine data when launched as a GUI App. What happens is that launching via the GUI requires the user to override GateKeeper by going to Security & Privacy > Open Anyway. Doing that updates the GateKeeper flags, and then the macOS API denies access: once the GK flags reach some state, quarantine information is not returned. This is not documented (as far as I can see) but moons ago, [somebody else on the internet witnessed the same thing](https://cocoa-dev.apple.narkive.com/kkYeAC8o/is-it-possible-to-read-your-own-quarantine-info-after-launch). To work around, we run the system SQLite binary, to fish the relevant information out of the per-user quarantine database. (SQLite is installed by default on all relevant macOS versions.) The most significant security concern I see is whether we can trust this binary (in /usr/bin/sqlite3). Some discussion within the Install/Update team suggested that an attacker who could corrupt or modify that binary already had write access to the disk, which is an attack vector equal to a totally compromised Firefox. If we determine that we can't use the system SQLite binary, then we could use Firefox's compiled copy of SQLite, but we might see versioning issues. The system SQLite binary feels more robust. This is implemented as a JS component for convenience, mostly: there is no API for capturing output from `nsIProcess`. It would be possible to maintain the existing XPCOM contract by renaming the existing contract and adding a contract with a JS implementation that passes through to the renamed implementation, but it doesn't seem worth the effort. In the next commits, we will generalize the existing caching mechanism form Windows to also apply to macOS. This is mostly a performance optimization, so that we sniff a single well-known location rather than launching a process at each startup, although there is a correctness argument here as well, since the quarantine database is dynamic and the attribution URL could expire. Differential Revision: https://phabricator.services.mozilla.com/D92693
200 lines
5.7 KiB
JavaScript
200 lines
5.7 KiB
JavaScript
/* 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/. */
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["AttributionCode"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"AppConstants",
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Services",
|
|
"resource://gre/modules/Services.jsm"
|
|
);
|
|
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
|
|
|
|
const ATTR_CODE_MAX_LENGTH = 1010;
|
|
const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/;
|
|
const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded &
|
|
const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded =
|
|
const ATTR_CODE_KEYS = [
|
|
"source",
|
|
"medium",
|
|
"campaign",
|
|
"content",
|
|
"experiment",
|
|
"variation",
|
|
"ua",
|
|
];
|
|
|
|
let gCachedAttrData = null;
|
|
|
|
/**
|
|
* Returns an nsIFile for the file containing the attribution data.
|
|
*/
|
|
function getAttributionFile() {
|
|
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
|
|
// appinfo does not exist in xpcshell, so we need defaults.
|
|
file.append(Services.appinfo.vendor || "mozilla");
|
|
file.append(AppConstants.MOZ_APP_NAME);
|
|
file.append("postSigningData");
|
|
return file;
|
|
}
|
|
|
|
var AttributionCode = {
|
|
/**
|
|
* Returns an array of allowed attribution code keys.
|
|
*/
|
|
get allowedCodeKeys() {
|
|
return [...ATTR_CODE_KEYS];
|
|
},
|
|
|
|
/**
|
|
* Returns an object containing a key-value pair for each piece of attribution
|
|
* data included in the passed-in attribution code string.
|
|
* If the string isn't a valid attribution code, returns an empty object.
|
|
*/
|
|
parseAttributionCode(code) {
|
|
if (code.length > ATTR_CODE_MAX_LENGTH) {
|
|
return {};
|
|
}
|
|
|
|
let isValid = true;
|
|
let parsed = {};
|
|
for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) {
|
|
let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2);
|
|
if (key && ATTR_CODE_KEYS.includes(key)) {
|
|
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
|
parsed[key] = value;
|
|
}
|
|
} else {
|
|
isValid = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isValid) {
|
|
return parsed;
|
|
}
|
|
|
|
Services.telemetry
|
|
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
|
|
.add("decode_error");
|
|
|
|
return {};
|
|
},
|
|
|
|
/**
|
|
* Reads the attribution code, either from disk or a cached version.
|
|
* Returns a promise that fulfills with an object containing the parsed
|
|
* attribution data if the code could be read and is valid,
|
|
* or an empty object otherwise.
|
|
*
|
|
* On windows the attribution service converts utm_* keys, removing "utm_".
|
|
* On OSX the attributions are set directly on download and retain "utm_". We
|
|
* strip "utm_" while retrieving the params.
|
|
*/
|
|
async getAttrDataAsync() {
|
|
if (gCachedAttrData != null) {
|
|
return gCachedAttrData;
|
|
}
|
|
|
|
gCachedAttrData = {};
|
|
if (AppConstants.platform == "win") {
|
|
let bytes;
|
|
try {
|
|
bytes = await OS.File.read(getAttributionFile().path);
|
|
} catch (ex) {
|
|
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
|
return gCachedAttrData;
|
|
}
|
|
Services.telemetry
|
|
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
|
|
.add("read_error");
|
|
}
|
|
if (bytes) {
|
|
try {
|
|
let decoder = new TextDecoder();
|
|
let code = decoder.decode(bytes);
|
|
gCachedAttrData = this.parseAttributionCode(code);
|
|
} catch (ex) {
|
|
// TextDecoder can throw an error
|
|
Services.telemetry
|
|
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
|
|
.add("decode_error");
|
|
}
|
|
}
|
|
} else if (AppConstants.platform == "macosx") {
|
|
const { MacAttribution } = ChromeUtils.import(
|
|
"resource:///modules/MacAttribution.jsm"
|
|
);
|
|
try {
|
|
let referrer = await MacAttribution.getReferrerUrl();
|
|
let params = new URL(referrer).searchParams;
|
|
for (let key of ATTR_CODE_KEYS) {
|
|
// We support the key prefixed with utm_ or not, but intentionally
|
|
// choose non-utm params over utm params.
|
|
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
|
|
if (params.has(paramKey)) {
|
|
// We expect URI-encoded components.
|
|
let value = encodeURIComponent(params.get(paramKey));
|
|
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
|
gCachedAttrData[key] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
// No attributions
|
|
}
|
|
}
|
|
return gCachedAttrData;
|
|
},
|
|
|
|
/**
|
|
* Return the cached attribution data synchronously without hitting
|
|
* the disk.
|
|
* @returns A dictionary with the attribution data if it's available,
|
|
* null otherwise.
|
|
*/
|
|
getCachedAttributionData() {
|
|
return gCachedAttrData;
|
|
},
|
|
|
|
/**
|
|
* Deletes the attribution data file.
|
|
* Returns a promise that resolves when the file is deleted,
|
|
* or if the file couldn't be deleted (the promise is never rejected).
|
|
*/
|
|
async deleteFileAsync() {
|
|
try {
|
|
await OS.File.remove(getAttributionFile().path);
|
|
} catch (ex) {
|
|
// The attribution file may already have been deleted,
|
|
// or it may have never been installed at all;
|
|
// failure to delete it isn't an error.
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clears the cached attribution code value, if any.
|
|
* Does nothing if called from outside of an xpcshell test.
|
|
*/
|
|
_clearCache() {
|
|
let env = Cc["@mozilla.org/process/environment;1"].getService(
|
|
Ci.nsIEnvironment
|
|
);
|
|
if (env.exists("XPCSHELL_TEST_PROFILE_DIR")) {
|
|
gCachedAttrData = null;
|
|
}
|
|
},
|
|
};
|