Changeset 5421ce1 in Klonkt


Ignore:
Timestamp:
06/27/2026 09:30:42 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
07de464
Parents:
ca0ad44
Message:

fix(csp): per-domain frame-src for cross-site embeds on the document (not authorize_interaction)

  • middleware/render.js — renderPage now appends the site's timeline origins (followed + boosted, from ap_timeline, 60s cache) to the document CSP frame-src for a logged-in site manager, so cross-site Klonkt embeds also load after an htmx/PWA navigation (the per-/news injection is ignored once you navigate via htmx). Excluded on pages/authorize-interaction — untrusted remote content next to the interact buttons, so no embeds / no frame-src loosening there (clickjacking).
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/middleware/render.js

    rca0ad44 r5421ce1  
    5353};
    5454
     55// Cross-site Klonkt audio embeds (a followed/boosted site's /embed player) need their
     56// origins in the DOCUMENT CSP frame-src — the per-/news injection is ignored once you
     57// navigate there via htmx (the document's CSP governs, not the partial's). Collect the
     58// distinct origins from the site's timeline (followed + boosted). Cached 60s.
     59const _embedOriginsCache = new Map(); // slug -> { origins, exp }
     60function embedOriginsFor(slug) {
     61  const now = Date.now();
     62  const c = _embedOriginsCache.get(slug);
     63  if (c && c.exp > now) return c.origins;
     64  let origins = [];
     65  try {
     66    const rows = db.prepare('SELECT DISTINCT url FROM ap_timeline WHERE slug = ? AND url IS NOT NULL').all(slug);
     67    const set = new Set();
     68    for (const r of rows) { try { set.add(new URL(r.url).origin); } catch { /* skip bad url */ } }
     69    origins = [...set];
     70  } catch { origins = []; }
     71  _embedOriginsCache.set(slug, { origins, exp: now + 60000 });
     72  return origins;
     73}
     74
    5575export async function renderPage(req, res, viewName, data = {}) {
    5676  // Decide: partial (HTMX) or full?
     
    93113  // to hide/disable write buttons (post, save, delete).
    94114  const _isViewer = isViewer(_u);
     115
     116  // Per-domain CSP: let a logged-in site manager frame the Klonkt players of the sites in
     117  // their timeline (followed + boosted) on the DOCUMENT, so cross-site embeds also work
     118  // after an htmx/PWA navigation. NEVER on authorize_interaction — that page shows
     119  // untrusted remote content next to the interact buttons, so no embeds / no frame-src
     120  // loosening there (clickjacking risk).
     121  try {
     122    if (_site && _site.slug && viewName !== 'pages/authorize-interaction' && _u && PermissionsService.canAdminSite(_u, _site)) {
     123      const origins = embedOriginsFor(_site.slug);
     124      if (origins.length) {
     125        const csp = res.getHeader('Content-Security-Policy');
     126        if (csp) res.setHeader('Content-Security-Policy', String(csp).replace(/frame-src ([^;]*)/i, (m, g) => `frame-src ${g} ${origins.join(' ')}`));
     127      }
     128    }
     129  } catch { /* CSP extension is best-effort */ }
    95130
    96131  // Who sees the "Admin" link? god/admin, a site owner (artist self-manage),
Note: See TracChangeset for help on using the changeset viewer.