mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(plugins): serve frame assets root-relative and cross-origin loadable
res.sendFile(absolutePath) resolves against the rewritten req.url under the Nest ExpressAdapter and 404s spuriously (files-download already works around the same trap), which broke every plugin frame document. And helmet's CORP: same-origin made the browser drop the opaque-origin frame's own script/style subresources, so a multi-file plugin client could never boot. Serve root-relative and mark frame responses cross-origin — sandbox + per-plugin CSP stay the isolation boundary.
This commit is contained in:
@@ -61,8 +61,18 @@ export class PluginFrameController {
|
||||
const ext = path.extname(resolved).toLowerCase();
|
||||
res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
// The frame document runs at an OPAQUE origin (sandbox without
|
||||
// allow-same-origin), so its own <script src>/<link> subresource loads are
|
||||
// cross-origin — helmet's default CORP: same-origin makes the browser drop
|
||||
// them (ERR_BLOCKED_BY_RESPONSE) and a multi-file client dies on boot.
|
||||
// The sandbox + per-plugin CSP are the isolation boundary here, not CORP:
|
||||
// these files are the plugin's own public client code.
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.setHeader('Content-Security-Policy', this.frameCsp(pluginId, req.get('host')));
|
||||
res.sendFile(resolved);
|
||||
// Explicit { root } + basename, not the absolute path: under the Nest
|
||||
// ExpressAdapter, res.sendFile(absolutePath) resolves against the rewritten
|
||||
// req.url and 404s spuriously (same trap as files-download.controller.ts).
|
||||
res.sendFile(path.basename(resolved), { root: path.dirname(resolved) });
|
||||
}
|
||||
|
||||
/** Per-plugin, locked-down CSP for the sandboxed frame document. */
|
||||
|
||||
@@ -33,10 +33,15 @@ function fakeRes() {
|
||||
headers: {} as Record<string, string>,
|
||||
sent: undefined as unknown,
|
||||
filePath: undefined as string | undefined,
|
||||
fileRoot: undefined as string | undefined,
|
||||
status(c: number) { res.statusCode = c; return res; },
|
||||
setHeader(k: string, v: string) { res.headers[k] = v; },
|
||||
send(b: unknown) { res.sent = b; return res; },
|
||||
sendFile(p: string) { res.filePath = p; res.statusCode ||= 200; },
|
||||
sendFile(p: string, opts?: { root?: string }) {
|
||||
res.filePath = opts?.root ? path.join(opts.root, p) : p;
|
||||
res.fileRoot = opts?.root;
|
||||
res.statusCode ||= 200;
|
||||
},
|
||||
};
|
||||
return res;
|
||||
}
|
||||
@@ -64,6 +69,23 @@ describe('PluginFrameController', () => {
|
||||
expect(res.headers['X-Content-Type-Options']).toBe('nosniff');
|
||||
});
|
||||
|
||||
// Under the Nest ExpressAdapter, res.sendFile(absolutePath) resolves against the
|
||||
// rewritten req.url and 404s spuriously — files must go out root-relative.
|
||||
it('serves files with an explicit root, not an absolute path', () => {
|
||||
const res = fakeRes();
|
||||
new PluginFrameController(runtime(true)).serve('widget', req(''), res as never);
|
||||
expect(res.fileRoot).toBe(path.join(codeRoot, 'widget', 'client'));
|
||||
});
|
||||
|
||||
// The frame document runs at an opaque origin, so its own <script src> loads are
|
||||
// cross-origin fetches; helmet's CORP: same-origin makes the browser drop them
|
||||
// and a multi-file client dies on boot.
|
||||
it('marks responses cross-origin loadable for the opaque frame', () => {
|
||||
const res = fakeRes();
|
||||
new PluginFrameController(runtime(true)).serve('widget', req(''), res as never);
|
||||
expect(res.headers['Cross-Origin-Resource-Policy']).toBe('cross-origin');
|
||||
});
|
||||
|
||||
// An operatorEgress plugin's hosts are supplied by the ADMIN after install, so they are not
|
||||
// in the manifest grants. activate() unions them into the child's egress guard; the frame
|
||||
// must match, or a plugin with a UI can call the host from its server but not its iframe.
|
||||
|
||||
Reference in New Issue
Block a user