diff --git a/packages/snaps-execution-environments/jest.config.js b/packages/snaps-execution-environments/jest.config.js index a071c2bf5c..85daac7887 100644 --- a/packages/snaps-execution-environments/jest.config.js +++ b/packages/snaps-execution-environments/jest.config.js @@ -6,10 +6,10 @@ module.exports = deepmerge(baseConfig, { coveragePathIgnorePatterns: ['./src/index.ts', '.ava.test.ts'], coverageThreshold: { global: { - branches: 83.68, - functions: 92.19, - lines: 87.11, - statements: 87.22, + branches: 83.93, + functions: 92.25, + lines: 87.07, + statements: 87.18, }, }, testEnvironment: '/jest.environment.js', diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.ts index c80883ac4a..e082126fef 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.ts @@ -642,6 +642,111 @@ describe('BaseSnapExecutor', () => { }); }); + it('allows direct access to ethereum public properties', async () => { + const CODE = ` + module.exports.onRpcRequest = () => { + const listener = () => undefined; + ethereum.on('accountsChanged', listener); + ethereum.removeListener('accountsChanged', listener); + return ethereum.request({ method: 'eth_blockNumber', params: [] }) }; + `; + const executor = new TestSnapExecutor(); + + await executor.executeSnap(1, FAKE_SNAP_NAME, CODE, ['ethereum']); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: 'OK', + }); + + await executor.writeCommand({ + jsonrpc: '2.0', + id: 2, + method: 'snapRpc', + params: [ + FAKE_SNAP_NAME, + ON_RPC_REQUEST, + FAKE_ORIGIN, + { jsonrpc: '2.0', method: '', params: [] }, + ], + }); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + method: 'OutboundRequest', + }); + + const blockNumRequest = await executor.readRpc(); + expect(blockNumRequest).toStrictEqual({ + name: 'metamask-provider', + data: { + id: expect.any(Number), + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }, + }); + + await executor.writeRpc({ + name: 'metamask-provider', + data: { + jsonrpc: '2.0', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: blockNumRequest.data.id!, + result: '0xa70e77', + }, + }); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + method: 'OutboundResponse', + }); + + expect(await executor.readCommand()).toStrictEqual({ + id: 2, + jsonrpc: '2.0', + result: '0xa70e77', + }); + }); + + it("doesn't allow direct access to ethereum internals", async () => { + const CODE = ` + module.exports.onRpcRequest = () => ethereum._rpcEngine.handle({ method: 'snap_confirm', params: [] }); + `; + const executor = new TestSnapExecutor(); + + await executor.executeSnap(1, FAKE_SNAP_NAME, CODE, ['ethereum']); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: 'OK', + }); + + await executor.writeCommand({ + jsonrpc: '2.0', + id: 2, + method: 'snapRpc', + params: [ + FAKE_SNAP_NAME, + ON_RPC_REQUEST, + FAKE_ORIGIN, + { jsonrpc: '2.0', method: '', params: [] }, + ], + }); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + error: { + code: -32603, + message: "Cannot read properties of undefined (reading 'handle')", + data: expect.any(Object), + }, + id: 2, + }); + }); + it('only allows certain methods in snap API', async () => { const CODE = ` module.exports.onRpcRequest = () => snap.request({ method: 'eth_blockNumber', params: [] }); diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts index 2636180a0a..617a88d130 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts @@ -411,7 +411,7 @@ export class BaseSnapExecutor { private createEIP1193Provider(provider: StreamProvider): StreamProvider { const originalRequest = provider.request.bind(provider); - provider.request = async (args) => { + const request = async (args: RequestArguments) => { assert( !args.method.startsWith('snap_'), ethErrors.rpc.methodNotFound({ @@ -428,7 +428,20 @@ export class BaseSnapExecutor { } }; - return provider; + // To harden and limit access to internals, we use a proxy. + const proxy = new Proxy(provider, { + get(target, prop: keyof StreamProvider) { + if (prop === 'request') { + return request; + } else if (['on', 'removeListener'].includes(prop)) { + return target[prop]; + } + + return undefined; + }, + }); + + return proxy; } /**