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
- Install
@devondragon/emdash-plugin-featured-image-studio in an EmDash site.
- Open Plugins → Featured Image Studio → Stock (Unsplash) tab.
- Search for anything (e.g. "llama").
- 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.mjs — buildEmDashCsp():
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
Summary
The strict Content-Security-Policy applied to
/_emdashroutes hardcodesimg-srcto'self' data: blob:(plus the marketplace origin) and does not extend it based on pluginallowedHosts. 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 becausehttps://images.unsplash.comis blocked.Related: #205 (same pattern, but for
connect-src+ S3). This is theimg-srcvariant of the same underlying issue.Repro
@devondragon/emdash-plugin-featured-image-studioin an EmDash site.https://images.unsplash.com/...againstimg-src 'self' data: blob: <marketplace>.Source
node_modules/emdash/dist/astro/middleware/auth.mjs—buildEmDashCsp():The plugin declares
network:fetchwithallowedHosts: ["api.unsplash.com", "images.unsplash.com"], butbuildEmDashCspnever consults plugin metadata — so the allowlist is effectively ignored for anything the browser fetches directly (images, fonts, etc.).Expected
Plugin-declared
allowedHostsshould be merged into the admin CSP for at leastimg-srcandconnect-src(likely gated on thenetwork:fetchcapability). That way a plugin declaringallowedHosts: ["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
/_emdashresponses: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