Skip to content
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

Open
wants to merge 126 commits into
base: LoginPerSite
Choose a base branch
from
Open

[SNAPSHOT] Plugin Beta Changes #1

wants to merge 126 commits into from

Conversation

lilyannehall
Copy link

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:

  • all default "ambient" APIs (like Fetch and XMLHttpRequest), and so this diff should be compared with an eye of whether the overall approach of isolating plugins and managing the way they communicate with sites and each other is sound.

Included plugin APIs are also each subject to individual consideration before going to production, and are in early stages of implementation. Currently those are:

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.

kumavis and others added 30 commits August 26, 2019 11:14
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
To allow passing the origin to restrictedMethods
type rpcMessageHandler = (origin:string, request:Object) =>
Promise<any>
danjm and others added 15 commits October 9, 2019 09:21
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
Copy link
Author

@lilyannehall lilyannehall left a 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)) {
Copy link
Author

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.

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?

Copy link
Author

@lilyannehall lilyannehall Nov 4, 2019

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?

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!

Comment on lines +192 to +194
// 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)
Copy link
Author

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?

Copy link
Author

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?

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).

Comment on lines 243 to 245
this.testProfile = {
name: 'Dan Finlay',
}
Copy link
Author

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +7 to +8
updatePluginState: 'Store data locally',
getPluginState: 'Get data stored locally',
Copy link
Author

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.

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.',
Copy link
Author

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.

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?

Copy link
Author

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?

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.

Copy link
Author

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.

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.

Comment on lines +9 to +22
const isTest = process.env.IN_TEST === 'true' || process.env.METAMASK_ENV === 'test'
const SES = (
isTest
? {
makeSESRootRealm: () => {
return {
evaluate: () => {
return () => true
},
}
},
}
: require('ses')
)
Copy link
Author

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?

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',
Copy link
Author

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)) {
Copy link
Author

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: () => {},
Copy link
Author

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?

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.

Comment on lines +372 to 373
// TODO: is this safe?
if (origin === 'MetaMask') {
Copy link
Author

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?

Copy link

@danfinlay danfinlay left a 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants