mirror of
https://github.com/penpot/penpot.git
synced 2026-08-19 03:08:40 +00:00
🐛 Fix plugin origin check breaking cross-origin plugin messaging
The origin check added in the previous commit compared event.origin against window.location.origin (Penpot own origin). Since plugins are cross-origin by design (hosted on the plugin author domain), this check rejected every legitimate message from every real plugin. The event.source-based sender routing (matching iframeWindow identity) is the correct and sufficient security mechanism - it cannot be forged cross-origin, so the redundant origin check was removed. - Removed event.origin check from load-plugin.ts message listener - Updated tests to use realistic plugin origins (localhost:4202/4203) and to verify rejection based on source identity, not origin - Fixed documentation examples: use event.source for receiving validation and '*' for postMessage targetOrigin AI-assisted-by: mimo-v2.5-pro
This commit is contained in:
parent
b04aa1d585
commit
d80cb5f2dc
@ -116,8 +116,8 @@ Your plugin can capture incoming messages from Penpot using the <code class="lan
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
// Validate the origin to ensure messages come from a trusted source
|
// Validate the source to ensure messages come from the parent (Penpot)
|
||||||
if (event.origin !== window.location.origin) {
|
if (event.source !== window.parent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Handle the incoming message
|
// Handle the incoming message
|
||||||
@ -133,11 +133,11 @@ This setup allows for two-way communication between Penpot and your plugin. Penp
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
// Sending a message back to Penpot from your plugin
|
// Sending a message back to Penpot from your plugin
|
||||||
parent.postMessage(responseMessage, window.location.origin);
|
parent.postMessage(responseMessage, "*");
|
||||||
```
|
```
|
||||||
|
|
||||||
-<code class="language-js">responseMessage</code> is the data you want to send back to Penpot.
|
-<code class="language-js">responseMessage</code> is the data you want to send back to Penpot.
|
||||||
-<code class="language-js">window.location.origin</code> should be used as the target origin to ensure messages are only sent to the intended recipient. Never use<code class="language-js">'*'</code> in production, as it allows any origin to receive the message.
|
- Using<code class="language-js">'*'</code> as the target origin is acceptable here because the message content is controlled by your plugin (the sender), not by untrusted input. If you know the exact Penpot origin, you can use it instead for stricter security.
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
|
|||||||
@ -127,6 +127,7 @@ describe('plugin-loader', () => {
|
|||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
},
|
},
|
||||||
iframeWindow: mockIframeWindow,
|
iframeWindow: mockIframeWindow,
|
||||||
|
manifest: { ...manifest, host: 'http://localhost:4202' },
|
||||||
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
||||||
|
|
||||||
vi.mocked(createPlugin).mockResolvedValue(mockPluginWithIframe);
|
vi.mocked(createPlugin).mockResolvedValue(mockPluginWithIframe);
|
||||||
@ -135,7 +136,7 @@ describe('plugin-loader', () => {
|
|||||||
|
|
||||||
const event = new MessageEvent('message', {
|
const event = new MessageEvent('message', {
|
||||||
data: 'test-message',
|
data: 'test-message',
|
||||||
origin: window.location.origin,
|
origin: 'http://localhost:4202',
|
||||||
});
|
});
|
||||||
Object.defineProperty(event, 'source', { value: mockIframeWindow });
|
Object.defineProperty(event, 'source', { value: mockIframeWindow });
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
@ -145,15 +146,17 @@ describe('plugin-loader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject messages from unrecognized origins', async () => {
|
it('should reject messages from unrecognized sources', async () => {
|
||||||
await loadPlugin(manifest);
|
await loadPlugin(manifest);
|
||||||
|
|
||||||
window.dispatchEvent(
|
const event = new MessageEvent('message', {
|
||||||
new MessageEvent('message', {
|
data: 'malicious-message',
|
||||||
data: 'malicious-message',
|
origin: 'https://evil.com',
|
||||||
origin: 'https://evil.com',
|
});
|
||||||
}),
|
Object.defineProperty(event, 'source', {
|
||||||
);
|
value: { nodeType: 999 } as unknown as Window,
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
expect(mockPluginApi.plugin.sendMessage).not.toHaveBeenCalled();
|
expect(mockPluginApi.plugin.sendMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -168,6 +171,7 @@ describe('plugin-loader', () => {
|
|||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
},
|
},
|
||||||
iframeWindow: mockIframeWindow1,
|
iframeWindow: mockIframeWindow1,
|
||||||
|
manifest: { ...manifest, host: 'http://localhost:4202' },
|
||||||
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
||||||
|
|
||||||
const mockPluginApi2 = {
|
const mockPluginApi2 = {
|
||||||
@ -176,6 +180,7 @@ describe('plugin-loader', () => {
|
|||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
},
|
},
|
||||||
iframeWindow: mockIframeWindow2,
|
iframeWindow: mockIframeWindow2,
|
||||||
|
manifest: { ...manifest, host: 'http://localhost:4203' },
|
||||||
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
||||||
|
|
||||||
vi.mocked(createPlugin).mockResolvedValue(mockPluginApi1);
|
vi.mocked(createPlugin).mockResolvedValue(mockPluginApi1);
|
||||||
@ -186,12 +191,13 @@ describe('plugin-loader', () => {
|
|||||||
|
|
||||||
const event = new MessageEvent('message', {
|
const event = new MessageEvent('message', {
|
||||||
data: 'test',
|
data: 'test',
|
||||||
origin: window.location.origin,
|
origin: 'http://localhost:4203',
|
||||||
});
|
});
|
||||||
Object.defineProperty(event, 'source', { value: mockIframeWindow2 });
|
Object.defineProperty(event, 'source', { value: mockIframeWindow2 });
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
expect(mockPluginApi2.plugin.sendMessage).toHaveBeenCalledWith('test');
|
expect(mockPluginApi2.plugin.sendMessage).toHaveBeenCalledWith('test');
|
||||||
|
expect(mockPluginApi1.plugin.sendMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load plugin using ɵloadPlugin', async () => {
|
it('should load plugin using ɵloadPlugin', async () => {
|
||||||
|
|||||||
@ -29,10 +29,6 @@ const closeAllPlugins = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (event.origin !== window.location.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const senderPlugin = plugins.find((it) => it.iframeWindow === event.source);
|
const senderPlugin = plugins.find((it) => it.iframeWindow === event.source);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user