mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
fix(widgets): put the CORP: cross-origin header on the route that actually serves content (#196)
Follow-up to #195. The CORP fix there landed on routes/content.js `/:id/file`, but that handler is SHADOWED: server.js registers a public `app.get('/api/content/:id/file')` (and `/thumbnail`) BEFORE the auth-gated content router, and that public route (gated by playlist/widget reference) is what actually serves widget logo/background images. So the header never changed on the wire — origin still returned CORP: same-origin and the player's sandboxed (opaque-origin) widget iframe kept getting NS_ERROR_DOM_CORP_FAILED / 0 bytes. Set Access-Control-Allow-Origin: * + Cross-Origin-Resource-Policy: cross-origin on the real public routes in server.js: /file, /thumbnail (local), and the remote-thumbnail proxy. Revert the now-dead content.js edit so the fix lives only where the bytes are served. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
178af029a4
commit
5f5ec88eb0
|
|
@ -347,12 +347,6 @@ router.get('/:id/file', (req, res) => {
|
|||
// Prevent path traversal
|
||||
const safePath = path.resolve(config.contentDir, path.basename(content.filepath));
|
||||
if (!safePath.startsWith(path.resolve(config.contentDir))) return res.status(403).json({ error: 'Invalid path' });
|
||||
// Widget boards (logo/background images) render inside the player's sandboxed
|
||||
// (opaque-origin) widget iframe, so these image requests are cross-origin. Without
|
||||
// CORP: cross-origin the helmet default (same-origin) blocks them (NS_ERROR_DOM_CORP_FAILED,
|
||||
// 0 bytes). Matches the /uploads/content static route. Content already serves publicly.
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.sendFile(safePath);
|
||||
});
|
||||
|
||||
|
|
@ -363,9 +357,6 @@ router.get('/:id/thumbnail', (req, res) => {
|
|||
if (!content.thumbnail_path) return res.status(404).json({ error: 'Thumbnail not found' });
|
||||
const safePath = path.resolve(config.contentDir, path.basename(content.thumbnail_path));
|
||||
if (!safePath.startsWith(path.resolve(config.contentDir))) return res.status(403).json({ error: 'Invalid path' });
|
||||
// See /:id/file — cross-origin so sandboxed widget iframes can load it.
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.sendFile(safePath);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -469,6 +469,13 @@ app.get('/api/content/:id/file', (req, res) => {
|
|||
if (!inPlaylist && !inWidget && !requesterCanAccessContent(req, content)) return res.status(403).json({ error: 'Content not assigned to any playlist or widget' });
|
||||
const safePath = path.resolve(config.contentDir, path.basename(content.filepath));
|
||||
if (!safePath.startsWith(path.resolve(config.contentDir))) return res.status(403).json({ error: 'Invalid path' });
|
||||
// Widget boards (logo / background images) render inside the player's sandboxed
|
||||
// (opaque-origin) widget iframe, so these image loads are cross-origin. The helmet
|
||||
// default CORP: same-origin blocks them (NS_ERROR_DOM_CORP_FAILED, 0 bytes). Allow
|
||||
// cross-origin — matches the /uploads/content static route; this content is already
|
||||
// served here without auth (playlist/widget-gated above).
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.sendFile(safePath);
|
||||
});
|
||||
|
||||
|
|
@ -488,6 +495,8 @@ async function proxyRemoteThumbnail(url, res) {
|
|||
const buf = Buffer.from(await upstream.arrayBuffer());
|
||||
res.set('Content-Type', ct);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.set('Cross-Origin-Resource-Policy', 'cross-origin'); // load in sandboxed widget iframes
|
||||
return res.send(buf);
|
||||
} catch (e) {
|
||||
return res.status(502).json({ error: 'Thumbnail fetch failed' });
|
||||
|
|
@ -513,6 +522,9 @@ app.get('/api/content/:id/thumbnail', (req, res) => {
|
|||
if (/^https?:\/\//i.test(content.thumbnail_path)) return proxyRemoteThumbnail(content.thumbnail_path, res);
|
||||
const safePath = path.resolve(config.contentDir, path.basename(content.thumbnail_path));
|
||||
if (!safePath.startsWith(path.resolve(config.contentDir))) return res.status(403).json({ error: 'Invalid path' });
|
||||
// See /file — cross-origin so sandboxed widget iframes can load it.
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.sendFile(safePath);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue