0
Fork 0
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:
Juanfran 2024-09-10 15:20:49 +02:00
parent 3e4bd85bf6
commit 120e9e5a04
7 changed files with 128 additions and 17 deletions

View file

@ -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();
});
});

View file

@ -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,

View file

@ -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');
});
});

View file

@ -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>) => {

View file

@ -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;

View file

@ -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) {

View file

@ -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;
},
};