mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
#254 lets an organization opt out of widget iframe isolation so that players can embed origin-strict third-party sites. It applied that opt-out to the widget editor's Preview as well. Preview is framed by the dashboard, from the dashboard's own origin, and the dashboard keeps its session JWT in localStorage. So with the setting on, anyone who can author a widget -- workspace_editor and up; viewers are refused at the create route -- could put script in a text widget and read the session of whichever admin clicked Preview. That is an editor -> admin escalation, and it is not the risk the confirmation modal asks the admin to accept: a player runs on a kiosk with a device token, an admin's dashboard session is a different thing entirely. The org setting is what makes players able to embed those sites, so the /render path keeps consulting it. Preview is pinned to allow-scripts in both places that build it -- the dashboard iframe and the server-side render -- so neither a frontend change nor a new server caller can re-grant it alone. Also correct the modal copy, which claimed same-origin would expose the session of anyone viewing "a display or preview". Preview is now excluded, and the display case is really the device token, so say that instead. widget-preview-stays-isolated.test.js fails if either half is reverted; both mutations were checked to fail before committing.
74 lines
3.1 KiB
JavaScript
74 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
// Guards the boundary added on top of #254 (org-level widget sandbox toggle).
|
|
//
|
|
// #254 let an org opt out of widget iframe isolation so PLAYERS can embed
|
|
// origin-strict third-party sites. As merged it applied the same opt-out to the
|
|
// widget editor's Preview — which is framed by the DASHBOARD, from the dashboard's
|
|
// own origin, where the admin's session JWT lives in localStorage. That turned
|
|
// "my kiosks are less isolated" into "anyone who can author a widget can lift the
|
|
// session of whichever admin clicks Preview" (workspace_editor and up; viewers are
|
|
// refused at the create route).
|
|
//
|
|
// These tests fail if the preview path ever consults the org setting again.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const WIDGETS_ROUTE = fs.readFileSync(path.join(__dirname, '..', 'routes', 'widgets.js'), 'utf8');
|
|
const WIDGETS_VIEW = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'frontend', 'js', 'views', 'widgets.js'),
|
|
'utf8'
|
|
);
|
|
|
|
// Body of a router handler, from its `router.<verb>('<route>'` to the next `router.`
|
|
function handlerBody(source, verb, route) {
|
|
const start = source.indexOf(`router.${verb}('${route}'`);
|
|
assert.notEqual(start, -1, `could not find router.${verb}('${route}') — test needs updating`);
|
|
const rest = source.slice(start + 1);
|
|
const end = rest.indexOf('\nrouter.');
|
|
return end === -1 ? rest : rest.slice(0, end);
|
|
}
|
|
|
|
test('preview sandbox constant is the isolating one', () => {
|
|
assert.match(
|
|
WIDGETS_ROUTE,
|
|
/const PREVIEW_IFRAME_SANDBOX = 'allow-scripts';/,
|
|
'PREVIEW_IFRAME_SANDBOX must be exactly allow-scripts (no allow-same-origin)'
|
|
);
|
|
});
|
|
|
|
for (const [verb, route] of [['post', '/preview'], ['post', '/preview-session']]) {
|
|
test(`${verb.toUpperCase()} ${route} pins the isolating sandbox and ignores the org setting`, () => {
|
|
const body = handlerBody(WIDGETS_ROUTE, verb, route);
|
|
assert.match(body, /PREVIEW_IFRAME_SANDBOX/, `${route} must render with PREVIEW_IFRAME_SANDBOX`);
|
|
assert.doesNotMatch(
|
|
body,
|
|
/widgetIframeSandboxForWorkspace/,
|
|
`${route} must NOT consult the org widget-sandbox setting — it renders in the dashboard origin`
|
|
);
|
|
});
|
|
}
|
|
|
|
test('player render path still honours the org setting (the feature itself)', () => {
|
|
const body = handlerBody(WIDGETS_ROUTE, 'get', '/:id/render');
|
|
assert.match(
|
|
body,
|
|
/widgetIframeSandboxForWorkspace\(widget\.workspace_id\)/,
|
|
'the /render path is what #254 is for — it must keep consulting the org setting'
|
|
);
|
|
});
|
|
|
|
test('dashboard preview iframe is hard-coded to allow-scripts', () => {
|
|
const iframeTag = WIDGETS_VIEW.match(/<iframe id="pvIframe"[^>]*>/);
|
|
assert.ok(iframeTag, 'could not find the #pvIframe preview iframe — test needs updating');
|
|
assert.match(iframeTag[0], /sandbox="allow-scripts"/, 'preview iframe must be statically sandboxed');
|
|
assert.doesNotMatch(
|
|
iframeTag[0],
|
|
/allow-same-origin|\$\{/,
|
|
'preview iframe sandbox must be a literal, not computed from the org setting'
|
|
);
|
|
});
|