Skip to content

Commit

Permalink
Add harden events utilities (#1221)
Browse files Browse the repository at this point in the history
* Add harden events utilities

Change lockdown execution order

WIP: Experimental testing (for debugging purposes only)

Update with version of code that will work after updating post-message-stream

Add review refactoring (1)

Add review refactoring (2)

Add review refactoring (3)

Update post-message-stream version and add test to improve coverage

Fix coverage after rebase

Try to fix coverage

Refactor lockdown-events.ts

Fix yarn conflicts after rebase

Refactor test for events lockdown function

Add lockdown events in other environments

Revert "Add lockdown events in other environments"

This reverts commit 4e89e4e.

Add new proposal for implementing lockdown of events

Fix coverage issues

* Fix after rebase
  • Loading branch information
david0xd authored Mar 15, 2023
1 parent 8dcb465 commit 6885e48
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/snaps-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@metamask/base-controller": "^2.0.0",
"@metamask/object-multiplex": "^1.1.0",
"@metamask/permission-controller": "^3.0.0",
"@metamask/post-message-stream": "^6.1.0",
"@metamask/post-message-stream": "^6.1.1",
"@metamask/rpc-methods": "^0.31.0",
"@metamask/snaps-execution-environments": "^0.31.0",
"@metamask/snaps-registry": "^1.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,58 @@ describe('IframeExecutionService', () => {
'Failed to access document of the snap iframe: SecurityError',
);
});

it('confirms that events are secured', async () => {
// Check if the security critical properties of the Event object
// are unavailable. This will confirm that executeLockdownEvents works
// inside snaps-execution-environments
const { service } = createService(IframeExecutionService, {
iframeUrl: new URL(IFRAME_URL),
});

await service.executeSnap({
snapId: MOCK_SNAP_ID,
sourceCode: `
module.exports.onRpcRequest = async ({ request }) => {
let result;
const promise = new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://metamask.io/');
xhr.send();
xhr.onreadystatechange = (ev) => {
result = ev;
resolve();
};
});
await promise;
return {
targetIsUndefined: result.target === undefined,
currentTargetIsUndefined: result.target === undefined,
srcElementIsUndefined: result.target === undefined,
composedPathIsUndefined: result.target === undefined
};
};
`,
endowments: ['console', 'XMLHttpRequest'],
});

const result = await service.handleRpcRequest(MOCK_SNAP_ID, {
origin: 'foo',
handler: HandlerType.OnRpcRequest,
request: {
jsonrpc: '2.0',
id: 1,
method: 'foobar',
params: [],
},
});

expect(result).toStrictEqual({
targetIsUndefined: true,
currentTargetIsUndefined: true,
srcElementIsUndefined: true,
composedPathIsUndefined: true,
});
});
});
8 changes: 4 additions & 4 deletions packages/snaps-execution-environments/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 77.41,
"functions": 91.26,
"lines": 85.29,
"statements": 85.37
"branches": 78.62,
"functions": 91.4,
"lines": 87.95,
"statements": 88.02
}
7 changes: 6 additions & 1 deletion packages/snaps-execution-environments/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const baseConfig = require('../../jest.config.base');
delete baseConfig.coverageThreshold;

module.exports = deepmerge(baseConfig, {
coveragePathIgnorePatterns: ['./src/index.ts', './src/common/test-utils'],
coveragePathIgnorePatterns: [
'./src/index.ts',
'./src/iframe/index.ts',
'./src/offscreen/index.ts',
'./src/common/test-utils',
],
coverageDirectory: './coverage/jest',
testTimeout: 10000,
testEnvironment: '<rootDir>/jest.environment.js',
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-execution-environments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"dependencies": {
"@metamask/object-multiplex": "^1.2.0",
"@metamask/post-message-stream": "^6.1.0",
"@metamask/post-message-stream": "^6.1.1",
"@metamask/providers": "^10.2.0",
"@metamask/rpc-methods": "^0.31.0",
"@metamask/snaps-utils": "^0.31.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// eslint-disable-next-line import/unambiguous
import { expect } from '@wdio/globals';

import { executeLockdownEvents } from './lockdown-events';

describe('lockdown events security', () => {
it('should lockdown events and made event properties inaccessible', async () => {
executeLockdownEvents();

const eventTarget = new EventTarget();

const promise = new Promise((resolve) => {
eventTarget.addEventListener('just-test-event', (eventObject) => {
// eslint-disable-next-line @typescript-eslint/unbound-method
resolve(eventObject.composedPath);
});
});

const testEvent = new Event('just-test-event');
eventTarget.dispatchEvent(testEvent);

const result = await promise;
expect(result).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// When creating a sandbox, limitation of the events from leaking
// sensitive objects is required. This is done by overriding own properties
// of prototypes of all existing events.
import { hasProperty } from '@metamask/utils';

/**
* Targeted Event objects and properties.
* Note: This is a map of the prototypes that inherit from Events with
* properties that are identified to leak sensitive objects.
* Not all browsers support all event types, so checking its existence is required.
*/
const targetEvents = new Map();
if (hasProperty(globalThis, 'UIEvent')) {
targetEvents.set(UIEvent.prototype, ['view']);
}
if (hasProperty(globalThis, 'MutationEvent')) {
targetEvents.set(MutationEvent.prototype, ['relatedNode']);
}
if (hasProperty(globalThis, 'MessageEvent')) {
targetEvents.set(MessageEvent.prototype, ['source']);
}
if (hasProperty(globalThis, 'FocusEvent')) {
targetEvents.set(FocusEvent.prototype, ['relatedTarget']);
}
if (hasProperty(globalThis, 'MouseEvent')) {
targetEvents.set(MouseEvent.prototype, [
'relatedTarget',
'fromElement',
'toElement',
]);
}
if (hasProperty(globalThis, 'TouchEvent')) {
targetEvents.set(TouchEvent.prototype, ['targetTouches', 'touches']);
}
if (hasProperty(globalThis, 'Event')) {
targetEvents.set(Event.prototype, [
'target',
'currentTarget',
'srcElement',
'composedPath',
]);
}

/**
* Attenuate Event objects by replacing its own properties.
*/
export function executeLockdownEvents() {
targetEvents.forEach((properties, prototype) => {
for (const property of properties) {
Object.defineProperty(prototype, property, {
value: undefined,
configurable: false,
writable: false,
});
}
});
}
2 changes: 2 additions & 0 deletions packages/snaps-execution-environments/src/iframe/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { executeLockdown } from '../common/lockdown/lockdown';
import { executeLockdownEvents } from '../common/lockdown/lockdown-events';
import { executeLockdownMore } from '../common/lockdown/lockdown-more';
import { IFrameSnapExecutor } from './IFrameSnapExecutor';

executeLockdown();
executeLockdownMore();
executeLockdownEvents();

IFrameSnapExecutor.initialize();
2 changes: 2 additions & 0 deletions packages/snaps-execution-environments/src/offscreen/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream';

import { executeLockdown } from '../common/lockdown/lockdown';
import { executeLockdownEvents } from '../common/lockdown/lockdown-events';
import { executeLockdownMore } from '../common/lockdown/lockdown-more';
import { OffscreenSnapExecutor } from './OffscreenSnapExecutor';

executeLockdown();
executeLockdownMore();
executeLockdownEvents();

// The stream from the offscreen document to the execution service.
const parentStream = new BrowserRuntimePostMessageStream({
Expand Down
2 changes: 1 addition & 1 deletion packages/snaps-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@metamask/eslint-config-nodejs": "^11.0.1",
"@metamask/eslint-config-typescript": "^11.0.0",
"@metamask/key-tree": "^7.0.0",
"@metamask/post-message-stream": "^6.1.0",
"@metamask/post-message-stream": "^6.1.1",
"@types/jest": "^27.5.1",
"@types/mocha": "^10.0.1",
"@types/semver": "^7.3.10",
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,7 @@ __metadata:
languageName: node
linkType: hard

"@metamask/post-message-stream@npm:^6.1.0":
"@metamask/post-message-stream@npm:^6.1.1":
version: 6.1.1
resolution: "@metamask/post-message-stream@npm:6.1.1"
dependencies:
Expand Down Expand Up @@ -3145,7 +3145,7 @@ __metadata:
"@metamask/eslint-config-typescript": ^11.0.0
"@metamask/object-multiplex": ^1.1.0
"@metamask/permission-controller": ^3.0.0
"@metamask/post-message-stream": ^6.1.0
"@metamask/post-message-stream": ^6.1.1
"@metamask/rpc-methods": ^0.31.0
"@metamask/snaps-execution-environments": ^0.31.0
"@metamask/snaps-registry": ^1.1.1
Expand Down Expand Up @@ -3228,7 +3228,7 @@ __metadata:
"@metamask/eslint-config-nodejs": ^11.0.1
"@metamask/eslint-config-typescript": ^11.0.0
"@metamask/object-multiplex": ^1.2.0
"@metamask/post-message-stream": ^6.1.0
"@metamask/post-message-stream": ^6.1.1
"@metamask/providers": ^10.2.0
"@metamask/rpc-methods": ^0.31.0
"@metamask/snaps-utils": ^0.31.0
Expand Down Expand Up @@ -3420,7 +3420,7 @@ __metadata:
"@metamask/eslint-config-typescript": ^11.0.0
"@metamask/key-tree": ^7.0.0
"@metamask/permission-controller": ^3.0.0
"@metamask/post-message-stream": ^6.1.0
"@metamask/post-message-stream": ^6.1.1
"@metamask/providers": ^10.2.1
"@metamask/snaps-registry": ^1.1.1
"@metamask/snaps-ui": ^0.31.0
Expand Down

0 comments on commit 6885e48

Please sign in to comment.