Skip to content

Admin CSP img-src doesn't extend from plugin allowedHosts (blocks Unsplash thumbnails in Featured Image Studio) #415

@devondragon

Description

@devondragon

Summary

The strict Content-Security-Policy applied to /_emdash routes hardcodes img-src to 'self' data: blob: (plus the marketplace origin) and does not extend it based on plugin allowedHosts. This breaks any plugin that renders remote thumbnails in the admin UI — notably @devondragon/emdash-plugin-featured-image-studio, whose Unsplash picker tab shows empty tiles because https://images.unsplash.com is blocked.

Related: #205 (same pattern, but for connect-src + S3). This is the img-src variant of the same underlying issue.

Repro

  1. Install @devondragon/emdash-plugin-featured-image-studio in an EmDash site.
  2. Open Plugins → Featured Image Studio → Stock (Unsplash) tab.
  3. Search for anything (e.g. "llama").
  4. Tiles render with titles/attribution/dimensions but the image areas are blank. DevTools console shows CSP violations for https://images.unsplash.com/... against img-src 'self' data: blob: <marketplace>.

Source

node_modules/emdash/dist/astro/middleware/auth.mjsbuildEmDashCsp():

function buildEmDashCsp(marketplaceUrl) {
    const imgSources = ["'self'", "data:", "blob:"];
    if (marketplaceUrl) try { imgSources.push(new URL(marketplaceUrl).origin); } catch {}
    return [
        "default-src 'self'",
        "script-src 'self' 'unsafe-inline'",
        "style-src 'self' 'unsafe-inline'",
        "connect-src 'self'",
        ...
        `img-src ${imgSources.join(" ")}`,
        ...
    ].join("; ");
}

The plugin declares network:fetch with allowedHosts: ["api.unsplash.com", "images.unsplash.com"], but buildEmDashCsp never consults plugin metadata — so the allowlist is effectively ignored for anything the browser fetches directly (images, fonts, etc.).

Expected

Plugin-declared allowedHosts should be merged into the admin CSP for at least img-src and connect-src (likely gated on the network:fetch capability). That way a plugin declaring allowedHosts: ["images.unsplash.com"] "just works" end-to-end without consumers having to patch CSP in their own middleware.

Workaround

In a consuming site's Astro middleware, patch the CSP on /_emdash responses:

if (pathname.startsWith("/_emdash")) {
  const csp = response.headers.get("Content-Security-Policy");
  if (csp && !csp.includes("images.unsplash.com")) {
    response.headers.set(
      "Content-Security-Policy",
      csp
        .replace(/img-src ([^;]*)/, "img-src $1 https://images.unsplash.com")
        .replace(/connect-src ([^;]*)/, "connect-src $1 https://api.unsplash.com")
    );
  }
}

This shouldn't be necessary — the plugin already told EmDash which hosts it needs.

Environment

  • emdash ^0.1.0
  • @devondragon/emdash-plugin-featured-image-studio ^0.2.0
  • Cloudflare Workers + D1 adapter

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions