Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class Extension {
*/
this.stack = new Stack(initializationData.data.stack, postRobot);

postRobot.on('extensionEvent', (event) => {
// Only the host (the window that embeds this iframe) may send events.
postRobot.on('extensionEvent', { window: window.parent }, (event) => {
if (event.data.name === 'entrySave') {
emitter.emitEvent('entrySave', [{ data: event.data.data }]);
emitter.emitEvent('updateFields', [{ data: event.data.data }]);
Expand Down
77 changes: 77 additions & 0 deletions test/hostOnlyEventsSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import testData from './data/testData.json';
import Extension from '../lib/extension.js';

// Security bug: extensionEvent must be accepted only from the host window
// (window.parent), never from a sibling iframe or a popup.
// Karma runs specs in an iframe, so window.parent is replaced with a separate
// frame's window to prove the listener is scoped to whatever the parent is.
describe('Extension host-only events', () => {
let parentDescriptor;
let hostFrame;
let siblingFrame;

const listeners = () => window.__postRobot__.requestListeners.extensionEvent;

beforeEach(() => {
// other specs build Extension too; post-robot allows one listener per window
delete window.__postRobot__.requestListeners.extensionEvent;
hostFrame = document.createElement('iframe');
siblingFrame = document.createElement('iframe');
document.body.appendChild(hostFrame);
document.body.appendChild(siblingFrame);
parentDescriptor = Object.getOwnPropertyDescriptor(window, 'parent');
Object.defineProperty(window, 'parent', { configurable: true, get: () => hostFrame.contentWindow });
});

afterEach(() => {
Object.defineProperty(window, 'parent', parentDescriptor);
hostFrame.remove();
siblingFrame.remove();
});

it('registers the extensionEvent listener for window.parent only', () => {
expect(window.parent).not.toBe(window);
// eslint-disable-next-line no-new
new Extension({ data: JSON.parse(JSON.stringify(testData)) });

expect(listeners().get(hostFrame.contentWindow)).toBeDefined();
expect(listeners().get(siblingFrame.contentWindow)).toBeUndefined();
expect(listeners().get(window.__postRobot__.WINDOW_WILDCARD)).toBeUndefined();
});

// Behaviour: a request-shaped message, as post-robot's own 'message' listener
// receives it, from the host is handled; the same message from a sibling is dropped.
let messageId = 0;
const deliver = (source, payload) => {
messageId += 1;
window.dispatchEvent(new MessageEvent('message', {
source,
origin: window.location.origin,
data: JSON.stringify({
__postRobot__: {
type: 'postrobot_message_request',
id: `host-only-spec-${messageId}`,
hash: `host-only-spec-${messageId}`,
name: 'extensionEvent',
sourceDomain: window.location.origin,
fireAndForget: true,
data: payload
}
})
}));
};

it('runs onSave for an entrySave from the host and ignores one from a sibling frame', (done) => {
const extension = new Extension({ data: JSON.parse(JSON.stringify(testData)) });
const saved = [];
extension.entry.onSave((data) => saved.push(data));

deliver(siblingFrame.contentWindow, { name: 'entrySave', data: { from: 'sibling' } });
deliver(hostFrame.contentWindow, { name: 'entrySave', data: { from: 'host' } });

setTimeout(() => {
expect(saved).toEqual([{ from: 'host' }]);
done();
}, 100);
});
});
Loading