mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-04 13:50:13 -05:00
fix(runtime): prevent override Penpot objects
This commit is contained in:
parent
3e4bd85bf6
commit
120e9e5a04
7 changed files with 128 additions and 17 deletions
|
@ -106,4 +106,14 @@ describe('createPlugin', () => {
|
|||
|
||||
expect(mockSandbox.evaluate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call plugin.close when there is an exception during sandbox evaluation', async () => {
|
||||
vi.mocked(mockSandbox.evaluate).mockImplementation(() => {
|
||||
throw new Error('Evaluation error');
|
||||
});
|
||||
|
||||
await createPlugin(mockContext, manifest, onCloseCallback);
|
||||
|
||||
expect(mockPluginManager.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,16 @@ export async function createPlugin(
|
|||
manifest: Manifest,
|
||||
onCloseCallback: () => void
|
||||
) {
|
||||
const evaluateSandbox = async () => {
|
||||
try {
|
||||
sandbox.evaluate();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
plugin.close();
|
||||
}
|
||||
};
|
||||
|
||||
const plugin = await createPluginManager(
|
||||
context,
|
||||
manifest,
|
||||
|
@ -16,12 +26,13 @@ export async function createPlugin(
|
|||
onCloseCallback();
|
||||
},
|
||||
function onReloadModal() {
|
||||
sandbox.evaluate();
|
||||
evaluateSandbox();
|
||||
}
|
||||
);
|
||||
|
||||
const sandbox = createSandbox(plugin);
|
||||
sandbox.evaluate();
|
||||
|
||||
evaluateSandbox();
|
||||
|
||||
return {
|
||||
plugin,
|
||||
|
|
|
@ -18,6 +18,7 @@ vi.mock('./ses.js', () => ({
|
|||
};
|
||||
}),
|
||||
harden: vi.fn().mockImplementation((obj) => obj),
|
||||
safeReturn: vi.fn().mockImplementation((obj) => obj),
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -90,22 +91,48 @@ describe('createSandbox', () => {
|
|||
expect(Object.keys(compartment.globalThis).length).toBe(0);
|
||||
});
|
||||
|
||||
it('should ensure fetch requests omit credentials', async () => {
|
||||
it('should ensure fetch requests omit credentials and return a harden response', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
url: 'https://example.com/api',
|
||||
text: vi.fn().mockResolvedValue('response text'),
|
||||
json: vi.fn().mockResolvedValue({ key: 'value' }),
|
||||
};
|
||||
|
||||
const sandbox = createSandbox(mockPlugin);
|
||||
const fetchSpy = vi
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(new Response());
|
||||
.mockResolvedValue(mockResponse as unknown as Response);
|
||||
|
||||
await sandbox.compartment.globalThis['fetch']('https://example.com/api', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('https://example.com/api', {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: '',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(ses.safeReturn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
url: 'https://example.com/api',
|
||||
text: expect.any(Function),
|
||||
json: expect.any(Function),
|
||||
})
|
||||
);
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
@ -122,4 +149,15 @@ describe('createSandbox', () => {
|
|||
Object.keys(sandbox.compartment.globalThis).filter((it) => !!it).length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('should return safe values for penpot methods via proxy', () => {
|
||||
const sandbox = createSandbox(mockPlugin);
|
||||
const mockPenpotMethod = vi.fn().mockReturnValue('penpot result');
|
||||
sandbox.compartment.globalThis['penpot'].mockMethod = mockPenpotMethod;
|
||||
|
||||
const result = sandbox.compartment.globalThis['penpot'].mockMethod();
|
||||
|
||||
expect(ses.safeReturn).toHaveBeenCalledWith('penpot result');
|
||||
expect(result).toBe('penpot result');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { Penpot } from '@penpot/plugin-types';
|
|||
import type { createPluginManager } from './plugin-manager';
|
||||
import { createApi } from './api';
|
||||
import { ses } from './ses.js';
|
||||
|
||||
export function createSandbox(
|
||||
plugin: Awaited<ReturnType<typeof createPluginManager>>
|
||||
) {
|
||||
|
@ -10,16 +9,51 @@ export function createSandbox(
|
|||
|
||||
const pluginApi = createApi(plugin);
|
||||
|
||||
const publicPluginApi = {
|
||||
penpot: ses.harden(pluginApi.penpot) as Penpot,
|
||||
fetch: ses.harden((...args: Parameters<typeof fetch>) => {
|
||||
const requestArgs: RequestInit = {
|
||||
...args[1],
|
||||
credentials: 'omit',
|
||||
const safeHandler = {
|
||||
get(target: Penpot, prop: string, receiver: unknown) {
|
||||
const originalValue = Reflect.get(target, prop, receiver);
|
||||
|
||||
if (typeof originalValue === 'function') {
|
||||
return function (...args: unknown[]) {
|
||||
const result = originalValue.apply(target, args);
|
||||
|
||||
return ses.safeReturn(result);
|
||||
};
|
||||
}
|
||||
|
||||
return ses.safeReturn(originalValue);
|
||||
},
|
||||
};
|
||||
|
||||
const proxyApi = new Proxy(pluginApi.penpot, safeHandler);
|
||||
|
||||
const safeFetch = (url: string, options: RequestInit) => {
|
||||
const sanitizedOptions: RequestInit = {
|
||||
...options,
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
...options?.headers,
|
||||
Authorization: '',
|
||||
},
|
||||
};
|
||||
|
||||
return fetch(url, sanitizedOptions).then((response) => {
|
||||
const safeResponse = {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url,
|
||||
text: response.text.bind(response),
|
||||
json: response.json.bind(response),
|
||||
};
|
||||
|
||||
return fetch(args[0], requestArgs);
|
||||
}),
|
||||
return ses.safeReturn(safeResponse);
|
||||
});
|
||||
};
|
||||
|
||||
const publicPluginApi = {
|
||||
penpot: proxyApi,
|
||||
fetch: ses.harden(safeFetch),
|
||||
console: ses.harden(window.console),
|
||||
Math: ses.harden(Math),
|
||||
setTimeout: ses.harden(
|
||||
|
@ -30,7 +64,7 @@ export function createSandbox(
|
|||
|
||||
plugin.timeouts.add(timeoutId);
|
||||
|
||||
return timeoutId;
|
||||
return ses.safeReturn(timeoutId);
|
||||
}
|
||||
) as typeof setTimeout,
|
||||
clearTimeout: ses.harden((id: ReturnType<typeof setTimeout>) => {
|
||||
|
|
|
@ -19,6 +19,12 @@ vi.mock('./create-plugin', () => ({
|
|||
createPlugin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./ses.js', () => ({
|
||||
ses: {
|
||||
harden: vi.fn().mockImplementation((obj) => obj),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('plugin-loader', () => {
|
||||
let mockContext: Context;
|
||||
let manifest: Manifest;
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { Context } from '@penpot/plugin-types';
|
|||
import { loadManifest } from './parse-manifest.js';
|
||||
import { Manifest } from './models/manifest.model.js';
|
||||
import { createPlugin } from './create-plugin.js';
|
||||
import { ses } from './ses.js';
|
||||
|
||||
let plugins: Awaited<ReturnType<typeof createPlugin>>[] = [];
|
||||
|
||||
|
@ -44,9 +45,13 @@ export const loadPlugin = async function (manifest: Manifest) {
|
|||
|
||||
closeAllPlugins();
|
||||
|
||||
const plugin = await createPlugin(context, manifest, () => {
|
||||
plugins = plugins.filter((api) => api !== plugin);
|
||||
});
|
||||
const plugin = await createPlugin(
|
||||
ses.harden(context) as Context,
|
||||
manifest,
|
||||
() => {
|
||||
plugins = plugins.filter((api) => api !== plugin);
|
||||
}
|
||||
);
|
||||
|
||||
plugins.push(plugin);
|
||||
} catch (error) {
|
||||
|
|
|
@ -13,4 +13,11 @@ export const ses = {
|
|||
harden: (obj: Object) => {
|
||||
return harden(obj);
|
||||
},
|
||||
safeReturn<T>(value: T): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return harden(value) as T;
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue