-
Notifications
You must be signed in to change notification settings - Fork 4
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
[SNAPSHOT] Plugin Beta Changes #1
base: LoginPerSite
Are you sure you want to change the base?
Conversation
remove provider approval controller integrate json-rpc-capabilities-middleware create PermissionsController move provider approval functionality to permissions controller add permissions approval ui permission requests still atomic but with multiple permissions permission approval UI added, still needs account permission logic rewrite provider.enable to use permissions controller provider-approval controller workflow runs without prompt after user approves permissions refactor clear approved origins to clear permissions move createStandardProvider to metamask-inpage-provider deprecate synchronous inpage methods remove getSiteMetadata; use site metadata now appended to requests by metamask-inpage-provider eth_requestAccounts -> eth_accounts; install latest json-rpc-capabilities-middleware eth_requestAccounts now handled by the inpage provider
basic wait-on-unlock implementatation in permissions controller
add checkbox selection to permission approval page begin add permissions settings page; edit messages revert isUnlocked behavior for now; it is managed both by the permissions and keyring controllers
ignore circleci e2e and coveralls; lint
show "connected" account in popup
…ailable to plugins
…d plugin request UI
Plugin system enhancements
Persist and restart plugins
…ollers available to plugins
Metamask events
To allow passing the origin to restrictedMethods
type rpcMessageHandler = (origin:string, request:Object) => Promise<any>
Allow plugins to handle RPC requests
…s without metamask_ prefix
Make `metamask_` prefix optional
Adds description for the getPluginState permission
Adds getTxById to restricted methods available to plugins
Add support for recipient address audit plugins
Security advisory: https://www.npmjs.com/advisories/1184 The security advisory was already addressed in commit 8e592df, but something about the lockfile seems to confuse the audit API (or we're running into a bug with resolutions). This resolution is simpler, and accomplishes the same goal of updating the vulnerable package.
Allows plugins to define a very flexible JS-based API like this: ``` wallet.registerApiRequestHandler((fromOrigin) => { return { ping: () => 'pong!', on: this.eventEmitter.on.bind(this.eventEmitter), } }) ``` All methods are converted to async methods per [capnode](https://www.npmjs.com/package/capnode). Sites can then request the flexible API like this: ``` async function requestIndex () { index = await ethereum.requestIndex() pluginApi = await index.getPluginApi(pluginOrigin) // Use the API provided by the plugin here! } ``` Includes a change to the `metamask-inpage-provider` that adds the `requestIndex()` method to it, for requesting a MetaMask capnode API. Adds a `requestPluginApi(pluginId)` method to the MetaMask index for requesting plugin APIs. Has weird issue: - [ ] Logs an inaccurate unauthorized error in dapp console when requesting permissions. Permissions request still succeeds.
Add plugin wallet.registerApiRequestHandler method
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.
Some thoughts, questions, and things we should should follow up on from my initial pass code review.
|
||
validateAsset (fromDomain, opts) { | ||
assetRequiredFields.forEach((requiredField) => { | ||
if (!(requiredField in opts)) { |
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 check will return true if the opts[requiredField]
property is declared but undefined. Example:
em@xps7390 ~ $ node
> const opts = { symbol: undefined, balance: null, identifier: function() {}, decimals: -19, customViewUrl: '' };
undefined
> const assetRequiredFields = ['symbol', 'balance', 'identifier', 'decimals', 'customViewUrl']
undefined
> assetRequiredFields.forEach((requiredField) => {
... if (!(requiredField in opts)) {
..... throw new Error('missing required field');
..... }
... })
undefined
>
Instead of (!(requiredField in opts))
use (typeof opts[requiredField] === 'undefined')
, as well as adding more sophisticated validation for the specific fields.
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 wasn't aware of this best practice. Can you tell me why it's better?
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.
@danfinlay in that example i posted up there you can see that using (foo in bar)
returns true
, even if bar[foo] === undefined
, because the the in
check only checks that the property exists - not the value. so if you don't check against undefined
(or check for general "truthiness" of the value) you can end up with null reference exceptions and similar issues because {foo:undefined}
will pass the check for 'foo' in bar
but not typeof bar[foo] !== 'undefined'
or even just !!bar[foo]
. am i explaining that clearly?
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.
Oh right, of course, we want to be validating for value presence. Thanks!
// We swallow this error, we don't want the plugin permissions prompt to block the resolution | ||
// Of the main dapp's permissions prompt. | ||
console.error(`Error when adding plugin:`, err) |
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.
If we swallow the error, does the user get notified that there was an issue?
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.
@danfinlay should the user be notified? could failing silently be bad?
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 requesting site would be the thing that doesn't get notified. It could break the site's ability to use the plugin, and will result in errors when trying to interact with the plugin in the future, so this seems like a passable interaction to me, but we could not respond to the dapp until all the plugins are installed. In that case we'd want to add some stronger UI to indicate to the user that installation is happening (which we'll surely add anyways).
this.testProfile = { | ||
name: 'Dan Finlay', | ||
} |
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 supposed to be here still?
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.
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 very much no longer needs to be here, we'll definitely be removing it too. Sorry this is def. a very early audit, some very drafty proof-of-concept code.
updatePluginState: 'Store data locally', | ||
getPluginState: 'Get data stored locally', |
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.
NOTE: I wonder if these can be used to donk up the state machine and cause problems.
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.
Yes, because it's part of the main state. I had forgotten to record this as a prod-blocker on this repo, but had opened an issue upstream. Now open here as I understand it: MetaMask/metamask-snaps-beta#88
getFilteredTxList: 'Get a list of filtered transactions', | ||
getGasPrice: 'Estimates a good gas price at recent prices', | ||
getTxById: 'Get full data of a transaction with a given metamask tx id', | ||
getState: 'Get a JSON representation of MetaMask data, including sensitive data. This is only for testing purposes and will NOT be included in production.', |
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 want to follow up on this and make sure that it doesn't get left in or otherwise is accessible.
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.
how does it get removed for production?
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.
it reads like "let's remember to remove this" but maybe @danfinlay knows something we don't?
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.
Yes, this section is basically entirely composed of permissions that we are including for the beta period, but will only include each after concluding it is useful and safe. It will be a manual process of actually grooming for production, and then re-introducing permissions deliberately.
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.
@danfinlay that's going to make it kind of tough for us to evaluate, since we don't really know what will and won't make it to prod. the only one that really sticks out to me as a big red flag is the getState
one which is obviously noted to included sensitive data. clearly, you all plan to remove it, but i think for the purposes of our audit, we probably need to operate under the "truth" that it is there now, so i'll open a ticket here.
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.
Yeah, I would say you should just ignore these individual permissions, sorry if I didn't make it clear. Rather than considering each one by one (which we will be doing before we accept any of these into production), I would recommend limiting the scope to a more framework-level consideration:
- Does the security model of granting permissions to a plugin make sense?
- Are there ways for a plugin to escalate the privileges it's been granted (excluding individual permissions).
If we were to include some permissions in the audit, I would limit the scope to ones we like enough to have documented in our wiki here.
const isTest = process.env.IN_TEST === 'true' || process.env.METAMASK_ENV === 'test' | ||
const SES = ( | ||
isTest | ||
? { | ||
makeSESRootRealm: () => { | ||
return { | ||
evaluate: () => { | ||
return () => true | ||
}, | ||
} | ||
}, | ||
} | ||
: require('ses') | ||
) |
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 it possible for another script to trick this into not using SES by setting global.process.env.IN_TEST
?
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 is a good concern, we should ensure we freeze the environment at boot time.
|
||
// TODO:SECURITY disable errorStackMode for production | ||
this.rootRealm = SES.makeSESRootRealm({ | ||
consoleMode: 'allow', errorStackMode: 'allow', mathRandomMode: 'allow', |
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 make sure this doesn't wiggle its way into prod
console.log(`Adding ${sourceUrl}`) | ||
|
||
// Deduplicate multiple add requests: | ||
if (!(pluginName in this.adding)) { |
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.
Avoid using (foo in bar)
truth check, since it checks initialization, not value.
const possibleApis = { | ||
updatePluginState: this.updatePluginState.bind(this, pluginName), | ||
getPluginState: this.getPluginState.bind(this, pluginName), | ||
onNewTx: () => {}, |
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 this supposed to be a noop?
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 looks like a stub that we considered implementing but haven't yet.
// TODO: is this safe? | ||
if (origin === 'MetaMask') { |
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.
Follow up on this. What passes origin
to this method? Can it be manipulated?
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.
Whoops I added comments under a review. Submitting so those comments go through.
I agree we can probably open a single issue with check marks to reflect the various points to hit.
This PR is a snapshot of the public PR at MetaMask/metamask-snaps-beta#77 for the purpose of private security review.
Originally from @danfinlay:
For reviewing relative to the
LoginPerSite
branch.Please note many of these changes are totally experimental and intended for removal, such as:
Included plugin APIs are also each subject to individual consideration before going to production, and are in early stages of implementation. Currently those are:
auditAddress
API.manageAsset
API.registerRpcMethodHandler
API.You can view known production blockers here.
As always, the wiki is a great place to start to understand the feature and its general API.