mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
fix: remove unsafe-eval from CSP and refactor extension loading
Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
f5b1fc038b
commit
923a16e024
4 changed files with 56 additions and 15 deletions
|
|
@ -23,7 +23,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost https://open.er-api.com https://api.coingecko.com https://query1.finance.yahoo.com https://query2.finance.yahoo.com https://api.frankfurter.dev"
|
"csp": "default-src 'self'; script-src 'self' blob:; style-src 'self'; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost https://open.er-api.com https://api.coingecko.com https://query1.finance.yahoo.com https://query2.finance.yahoo.com https://api.frankfurter.dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|
|
||||||
|
|
@ -40,14 +40,37 @@ describe("extensions", () => {
|
||||||
let consoleErrorSpy: any;
|
let consoleErrorSpy: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal("window", { __numi_api: undefined });
|
||||||
|
vi.stubGlobal("Blob", class Blob {
|
||||||
|
parts: any[];
|
||||||
|
options: any;
|
||||||
|
constructor(parts: any[], options: any) { this.parts = parts; this.options = options; }
|
||||||
|
});
|
||||||
|
vi.stubGlobal("URL", {
|
||||||
|
createObjectURL: vi.fn((blob: any) => "blob:" + blob.parts[0]),
|
||||||
|
revokeObjectURL: vi.fn()
|
||||||
|
});
|
||||||
|
vi.stubGlobal("document", {
|
||||||
|
createElement: vi.fn(() => ({ src: "", onload: null, onerror: null })),
|
||||||
|
head: { appendChild: vi.fn((script: any) => {
|
||||||
|
try {
|
||||||
|
const code = script.src.replace(/^blob:/, "");
|
||||||
|
eval(code);
|
||||||
|
if (script.onload) script.onload();
|
||||||
|
} catch(e) {
|
||||||
|
if (script.onerror) script.onerror();
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
});
|
||||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
consoleErrorSpy.mockRestore();
|
consoleErrorSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs scripts successfully using numi api", () => {
|
it("runs scripts successfully using numi api", async () => {
|
||||||
const scripts = [
|
const scripts = [
|
||||||
{
|
{
|
||||||
name: "script1",
|
name: "script1",
|
||||||
|
|
@ -59,14 +82,14 @@ describe("extensions", () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
runExtensions(mockEngine as SumEngine, scripts);
|
await runExtensions(mockEngine as SumEngine, scripts);
|
||||||
|
|
||||||
expect(mockEngine.setVariable).toHaveBeenCalledWith("var1", 10);
|
expect(mockEngine.setVariable).toHaveBeenCalledWith("var1", 10);
|
||||||
expect(mockEngine.addUnit).toHaveBeenCalledWith({ id: "unit1", phrases: "u", baseUnitId: "USD", ratio: 1 });
|
expect(mockEngine.addUnit).toHaveBeenCalledWith({ id: "unit1", phrases: "u", baseUnitId: "USD", ratio: 1 });
|
||||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("swallows errors and logs to console.error", () => {
|
it("swallows errors and logs to console.error", async () => {
|
||||||
const scripts = [
|
const scripts = [
|
||||||
{
|
{
|
||||||
name: "script1",
|
name: "script1",
|
||||||
|
|
@ -74,7 +97,7 @@ describe("extensions", () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
runExtensions(mockEngine as SumEngine, scripts);
|
await runExtensions(mockEngine as SumEngine, scripts);
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
"extension script1 failed:",
|
"extension script1 failed:",
|
||||||
|
|
@ -82,7 +105,7 @@ describe("extensions", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("continues running other scripts if one fails", () => {
|
it("continues running other scripts if one fails", async () => {
|
||||||
const scripts = [
|
const scripts = [
|
||||||
{
|
{
|
||||||
name: "script1",
|
name: "script1",
|
||||||
|
|
@ -98,7 +121,7 @@ describe("extensions", () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
runExtensions(mockEngine as SumEngine, scripts);
|
await runExtensions(mockEngine as SumEngine, scripts);
|
||||||
|
|
||||||
expect(mockEngine.setVariable).toHaveBeenCalledWith("var1", 10);
|
expect(mockEngine.setVariable).toHaveBeenCalledWith("var1", 10);
|
||||||
expect(mockEngine.setVariable).toHaveBeenCalledWith("var3", 30);
|
expect(mockEngine.setVariable).toHaveBeenCalledWith("var3", 30);
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,32 @@ export function makeApi(engine: SumEngine): ExtensionApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runExtensions(engine: SumEngine, scripts: { name: string; code: string }[]): void {
|
export async function runExtensions(engine: SumEngine, scripts: { name: string; code: string }[]): Promise<void> {
|
||||||
const numi = makeApi(engine);
|
const numi = makeApi(engine);
|
||||||
|
(window as any).__numi_api = numi;
|
||||||
for (const s of scripts) {
|
for (const s of scripts) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
try {
|
try {
|
||||||
const fn = new Function("numi", s.code);
|
const wrappedCode = `try { (function(numi) { ${s.code} })(window.__numi_api); } catch(e) { window.__numi_err = e; }`;
|
||||||
fn(numi);
|
const blob = new Blob([wrappedCode], { type: "text/javascript" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = url;
|
||||||
|
script.onload = () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
if ((window as any).__numi_err) {
|
||||||
|
console.error(`extension ${s.name} failed:`, (window as any).__numi_err);
|
||||||
|
delete (window as any).__numi_err;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
script.onerror = () => { URL.revokeObjectURL(url); resolve(); };
|
||||||
|
document.head.appendChild(script);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`extension ${s.name} failed:`, e);
|
console.error(`extension ${s.name} failed:`, e);
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
delete (window as any).__numi_api;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -745,7 +745,7 @@ async function initDataAndEngine(): Promise<void> {
|
||||||
if (!data.docs.some((d) => d.id === data.activeId)) data.activeId = data.docs[0].id;
|
if (!data.docs.some((d) => d.id === data.activeId)) data.activeId = data.docs[0].id;
|
||||||
|
|
||||||
const scripts = await loadExtensionScripts();
|
const scripts = await loadExtensionScripts();
|
||||||
runExtensions(engine, scripts);
|
await runExtensions(engine, scripts);
|
||||||
|
|
||||||
workspace = new Workspace(engine, () =>
|
workspace = new Workspace(engine, () =>
|
||||||
data.docs.map((d) => ({ id: d.id, title: d.title, text: data.contents[d.id] ?? "" })),
|
data.docs.map((d) => ({ id: d.id, title: d.title, text: data.contents[d.id] ?? "" })),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue