-
Notifications
You must be signed in to change notification settings - Fork 338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add --chromium-pref
flag
#2912
base: master
Are you sure you want to change the base?
Conversation
@@ -66,6 +66,9 @@ describe('util/extension-runners/chromium', async () => { | |||
'--password-store=basic', | |||
'--use-mock-keychain', | |||
'--force-fieldtrials=*BackgroundTracing/default/', | |||
'--disable-hang-monitor', | |||
'--disable-prompt-on-repost', | |||
'--disable-domain-reliability', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes were required after upgrading chrome-launcher, similar to #2825 (comment)
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #2912 +/- ##
===========================================
+ Coverage 0 99.31% +99.31%
===========================================
Files 0 32 +32
Lines 0 1759 +1759
===========================================
+ Hits 0 1747 +1747
- Misses 0 12 +12 ☔ View full report in Codecov by Sentry. |
f6cda7b
to
6442c6c
Compare
Rebased and fixed merge conflicts |
4d10f82
to
c8dea96
Compare
Rebased and fixed conflicts |
No conflicts present with Branch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add
@aklinker1 why not merge this pr? I still encounter the content script source map issues |
Only maintainers can merge PRs, and this hasn't been approved by one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks
@VersoriumX I appreciate your approvals, but since you're not a maintainer, you approving this more than once doesn't really help |
Sorry for the delay here - I've put this on my list of tasks to review. We are supportive of the requested capability here. |
@Rob--W no worries! Thanks for the update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR contains unrelated code (maybe partially due to a merge conflict).
Since you need to fix up the PR, could you split this PR in two parts:
chore: Update chrome-launcher
(this can be merged quickly)feat: Add --chromium-pref flag
(can be reviewed and merged separately).
If it makes it easier, you could create a new branch branched off the main
branch that updates chrome-launcher (and create a PR for that), and then rebase the current branch on top of that. Once the chrome-launcher PR is merged into main, this PR can be rebased on top of the main branch.
package.json
Outdated
@@ -81,6 +81,7 @@ | |||
"open": "9.1.0", | |||
"parse-json": "7.1.1", | |||
"promise-toolbox": "0.21.0", | |||
"set-value": "4.1.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not add the set-value
module. It appears to be unmaintained (no responses from the project maintainer on several comments from the past year), and upon inspection I also see the lack of safeguards against pollution of built-in prototype members.
src/program.js
Outdated
requiresArg: true, | ||
type: 'array', | ||
coerce: (arg) => | ||
arg != null ? coerceCLICustomPreference(arg) : undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coerceCLICustomPreference
is Firefox-specific, and includes Firefox-specific logic such as preferences that cannot be overridden. You should use a custom one for Chromium.
src/extension-runners/chromium.js
Outdated
* "extensions.ui.developer_mode". | ||
*/ | ||
getPrefs() { | ||
return Object.entries({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid the dependency of set-value
and avoid overwriting built-in instance members, let's roll (y)our own. The expected input is a list of dotted paths + values, currently stored as a flat object (but originally a string of dotted.key=value
pairs). The expected output is an object with nested objects.
Here is an example (not tested):
function mapToObject(map) {
return Object.fromEntries(
Array.from(
map,
([k, v]) => [k, v instanceof Map ? mapToObject(v) : v]
)
);
}
// Logic for getPrefs(), given prefs = DEFAUL_PREFS etc.
const prefsMap = new Map();
for (let [key, value] of prefs) {
let submap = prefsMap;
const props = key.split(".");
const lastProp = props.pop();
for (let prop of props) {
if (!submap.has(prop)) {
submap.set(prop, new Map());
}
submap = submap.get(prop);
if (!(submap instanceof Map)) {
throw new Error(`Cannot set ${key} because a value already exists at ${prop}`);
}
}
// TODO: Consider log.warn() if submap.has(lastProp) before overwriting.
submap.set(lastProp, value);
}
return mapToObject(prefsMap);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to base the function off this implementation. It's very compact and doesn't require using maps: https://youmightnotneed.com/lodash/#set
Will add some basic tests too.
Why do you want to avoid overwriting existing values? None of the default prefs I set in this PR are necessary for running the extension, they're just nice defaults that someone could turn off if they wanted. I also can't think of a use-case in the future where that would be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to base the function off this implementation. It's very compact and doesn't require using maps: https://youmightnotneed.com/lodash/#set
This snippet you've selected and lodash (_.set
) are both vulnerable to a Prototype pollution vulnerability. The set-value
library that you used before fixed that in response to a report (GHSA-4g88-fppr-53pp). Despite the fix, it is still possible to overwrite properties on inherit built-in prototypes. For example:
var setValue = require('set-value');
setValue({}, 'hasOwnProperty.call', 1);
// Expected: true. Actual: TypeError: Object.prototype.hasOwnProperty.call is not a function
console.log(Object.prototype.hasOwnProperty.call(Math, "min"));
Why do you want to avoid overwriting existing values?
Are you referring to "TODO: Consider log.warn() if submap.has(lastProp) before overwriting."? The purpose of that is to let the user know that a built-in value has been overwritten. Not that big of a deal. I'm also fine with omitting it. Overwriting the value when there is a Map
at that point may be an error though, consider the following:
download.download_path=/path/to/somewhere
download=1
It is not meaningful to set "download" to 1, and a warning could be nice. Not required though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see now. I misunderstood lots of things about the original comment lol. But now that I've implemented and tested your original suggestion, we're on the same page.
Only comment is about throwing the error. See these test cases:
We throw an error on the first, but not the second. I think we should make both behave the same way, either throw an error or allow the later value to override the earlier. Thoughts?
5f79a77
to
61e8a86
Compare
getPrefs() { | ||
return expandPrefs({ | ||
...DEFAULT_PREFS, | ||
...(this.params.customChromiumPrefs || {}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spread syntax with ...undefined
and ...null
works, you don't need to fall back to || {}
.
...(this.params.customChromiumPrefs || {}), | |
...this.params.customChromiumPrefs, |
|
||
// Create an alias for --chromium-pref since it has been transformed into an | ||
// object containing one or more preferences. | ||
const customChromiumPrefs = { ...chromiumPref }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that you basically copied the previous few lines. The logic there did not completely make sense, I've sent a PR to fix that up: #3218
Let's simplify to the following:
const customChromiumPrefs = { ...chromiumPref }; | |
const customChromiumPrefs = chromiumPref; |
}; | ||
const actual = expandPrefs(input); | ||
|
||
assert.notEqual(Object.prototype.hasOwnProperty.call, call); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of notEqual, save the original value before modifying the test, then do a strict comparison. A unit test that tests for equality is stronger than one that tests for inequality.
assert.deepEqual(actual, expected); | ||
}); | ||
|
||
it("doesn't pollute the object prototype", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test case here is a specific one that failed the original library that you proposed. Let's also prepend a test for a more obvious test case:
const input = {
'__proto__.x': 1
};
// Using JSON.parse because an object literal with __proto__
// would be mistaken for an object with a different prototype.
const expected = JSON.parse('{"__proto__":{"x":1}}');
Besides the obvious comparison like above, you should also add a test Object.getPrototypeOf(actual)
and confirm that it is Object.prototype
.
And let's also have the same test, but with the following:
const input = JSON.parse('{"__proto__":[]}');
const expected = JSON.parse('{"__proto__":[]}');
params: { | ||
customChromiumPrefs: { | ||
'download.default_directory': '/some/directory', | ||
'extensions.ui.developer_mode': false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's split the test in two: that a custom pref can be passed, and that a default pref can be overridden.
We want to make sure that custom prefs are merged with the default prefs, and not changed. The current implementation is correct, but the test as constructed would still pass if the implementation failed to merge, which is a sign of an incomplete test.
import { UsageError } from '../errors.js'; | ||
|
||
export function coerceCLICustomChromiumPreference(cliPrefs) { | ||
const customPrefs = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const customPrefs = {}; | |
const customPrefs = new Map(); |
Suggesting a few changes to avoid prototype pollution.
value = value === 'true'; | ||
} | ||
|
||
customPrefs[`${key}`] = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
customPrefs[`${key}`] = value; | |
customPrefs.set(`${key}`, value); |
customPrefs[`${key}`] = value; | ||
} | ||
|
||
return customPrefs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return customPrefs; | |
return Object.fromEntries(customPrefs); |
const key = prefsAry[0]; | ||
let value = prefsAry.slice(1).join('='); | ||
|
||
if (/[^\w_.]/.test(key)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any point in this check or can it be dropped?
\w
means [A-Za-z0-9_]
, so what your code here does is rejecting anything other than alphanumeric characters and period.
It is stricter than the actual values permitted by Chromium's Preferences keys, which is quite permissive actually. For example it can contain URLs (including :
characters which are rejected by your current code), e.g. as seen at:
- example of file: https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/data/diagnostics/user/Default/Preferences;l=58;drc=f0f6afc99205acab3f069d915cdad756a52d8809
- example in code comment: https://source.chromium.org/chromium/chromium/src/+/main:net/http/http_server_properties_manager.cc;l=245-256;drc=4787fce6c51383f5631643ac3d14cc512d656de6
@VersoriumX squssh and merge |
Overview
This closes #2899.
Add a new parameter,
--chromium-pref
, which lets users configure some profile settings when running theweb-ext run
command. For default preferences, I setextensions.ui.developer_mode=true
so you don't have to toggle the developer mode switch every time you run an extension on chromium.web-ext/src/extension-runners/chromium.js
Lines 30 to 32 in 9d0512a
Manual Testing
Here's the commands I ran to do manual testing: