From 827b1c4c87c84a5702b8ebcd4519ad435b81be9a Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Mon, 8 Jun 2026 23:36:53 -0500 Subject: [PATCH] fix(widgets): make widget/kiosk render frameable (X-Frame-Options) The web player embeds widget/kiosk renders in a sandboxed (allow-scripts, no allow-same-origin) iframe = a null origin. The global helmet X-Frame-Options: SAMEORIGIN refuses that (null != same-origin), so every widget rendered blank in the web player (video worked since it isn't an iframe). Drop X-Frame-Options on just the /render endpoints - the sandbox, not X-Frame-Options, is what isolates the widget from the dashboard (it still can't read the JWT). Dashboard keeps its clickjacking protection. Verified: directory board now renders in a sandboxed iframe with no refusal. --- server/routes/kiosk.js | 3 +++ server/routes/widgets.js | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/server/routes/kiosk.js b/server/routes/kiosk.js index 0ba2502..bf60d32 100644 --- a/server/routes/kiosk.js +++ b/server/routes/kiosk.js @@ -180,6 +180,9 @@ router.get('/:id/render', (req, res) => { `; + // Embedded by the player in a sandboxed (null-origin) iframe; the global + // X-Frame-Options: SAMEORIGIN would refuse that and leave it blank. + res.removeHeader('X-Frame-Options'); res.setHeader('Content-Type', 'text/html'); res.send(html); }); diff --git a/server/routes/widgets.js b/server/routes/widgets.js index c16751a..119abcd 100644 --- a/server/routes/widgets.js +++ b/server/routes/widgets.js @@ -183,6 +183,12 @@ router.get('/:id/render', (req, res) => { const widget = db.prepare('SELECT * FROM widgets WHERE id = ?').get(req.params.id); if (!widget) return res.status(404).send('Widget not found'); const config = JSON.parse(widget.config || '{}'); + // This page is DESIGNED to be embedded by the player, which frames it in a + // sandboxed (allow-scripts, no allow-same-origin) iframe = a null origin. The + // global helmet X-Frame-Options: SAMEORIGIN refuses that (null != same), so + // widgets render blank in the web player. Drop it here; the sandbox - not + // X-Frame-Options - is what isolates the widget (it can't read the dashboard JWT). + res.removeHeader('X-Frame-Options'); res.setHeader('Content-Type', 'text/html'); res.send(renderWidgetHtml(widget.widget_type, config)); });