Satori 0.29.0 added SSRF protection for server-side image fetching in #769. Thank you for addressing this security risk.
Unfortunately, the new protection unconditionally blocks localhost, with no supported way for an application to opt out, allow a trusted host, or provide its own fetch implementation. This prevents us from upgrading from 0.28.1.
Our application uses Satori to generate Open Graph images. In local development, application assets are served by our local development server:
http://localhost:5555/images/opengraph-background.png
After upgrading to 0.29.0, local rendering fails with:
Error: Image source resolves to a blocked address (SSRF protection):
http://localhost:5555/images/opengraph-background.png
at assertSafeServerFetchUrl (...)
at resolveImageData (...)
These are trusted, repository-owned static assets served by our local development environment. The same code uses a public HTTPS asset origin in production.
Impact
This breaks all local development and testing of the affected Open Graph image-rendering paths. Developers can no longer accurately test these images using the same local application and asset server used by the rest of the project.
Because there is no supported escape hatch, our current choices are to:
- remain on Satori 0.28.1;
- patch Satori locally;
- rewrite our image-loading pipeline; or
- configure local development to fetch assets from a remote deployment, which means developers are no longer testing their local assets.
We do not want to remove SSRF protection in production, and we would not enable any reduced-security behavior there. We only need an explicit way to support trusted private destinations in a controlled local environment.
Possible solutions
We would be happy with any API that preserves the secure default while allowing the embedding application to make an explicit, local-only decision.
1. Per-render private-network opt-out
For example:
await satori(element, {
width,
height,
fonts,
dangerouslyAllowPrivateNetwork: process.env.NODE_ENV !== "production",
});
The option should default to false, with documentation explaining that it must only be enabled when all image sources are trusted.
A narrower variation could permit loopback without permitting every private network:
allowLoopbackImageSources: process.env.NODE_ENV !== "production",
2. Per-image-request allow callback
A callback would let applications make a more targeted decision:
await satori(element, {
width,
height,
fonts,
allowUnsafeImageUrl(url) {
return (
process.env.NODE_ENV !== "production" &&
new URL(url).hostname === "localhost"
);
},
});
Satori could continue applying its normal protection unless the callback explicitly permits the individual URL.
This could also be expressed as an allowlist:
allowedImageHosts:
process.env.NODE_ENV !== "production" ? ["localhost"] : [],
A callback is more flexible, but either form would let us authorize only the local asset server rather than disabling the checker entirely.
3. Pluggable image fetcher
The initial version of #769 included a custom fetcher, but it was removed because a typeof fetch signature would allow caller-controlled Request and redirect behavior and would not compose safely with hardened fetch libraries.
A narrower signature could avoid that concern:
type ImageFetcher = (url: string) => Promise<Response>;
await satori(element, {
width,
height,
fonts,
fetcher: applicationSafeFetch,
});
Satori would provide only the URL string, leaving redirect policy and network enforcement entirely to the application-supplied implementation.
Our application already has a hardened server-side fetch implementation that:
- blocks private, loopback, link-local, and reserved addresses in deployed environments;
- validates resolved addresses at connection time;
- applies the same protection to redirects; and
- explicitly permits private destinations only in the local environment.
Allowing us to provide that implementation would both restore local development and provide stronger production protection than a literal-host check alone.
An image resolver returning data rather than a Response could be another option:
type ImageResolver = (url: string) => Promise<ArrayBuffer | string>;
Security expectations
We are not requesting that localhost be allowed by default.
Any escape hatch could:
- retain the current secure behavior by default;
- require an explicit, prominently named option;
- document the SSRF consequences;
- be configured independently for each Satori render; and
- allow applications to keep the protection enabled in production.
In our case, reduced protection would only be enabled in local development. Production uses public HTTPS asset origins and would continue using SSRF enforcement or our application’s hardened fetcher.
Satori 0.29.0 added SSRF protection for server-side image fetching in #769. Thank you for addressing this security risk.
Unfortunately, the new protection unconditionally blocks
localhost, with no supported way for an application to opt out, allow a trusted host, or provide its own fetch implementation. This prevents us from upgrading from 0.28.1.Our application uses Satori to generate Open Graph images. In local development, application assets are served by our local development server:
After upgrading to 0.29.0, local rendering fails with:
These are trusted, repository-owned static assets served by our local development environment. The same code uses a public HTTPS asset origin in production.
Impact
This breaks all local development and testing of the affected Open Graph image-rendering paths. Developers can no longer accurately test these images using the same local application and asset server used by the rest of the project.
Because there is no supported escape hatch, our current choices are to:
We do not want to remove SSRF protection in production, and we would not enable any reduced-security behavior there. We only need an explicit way to support trusted private destinations in a controlled local environment.
Possible solutions
We would be happy with any API that preserves the secure default while allowing the embedding application to make an explicit, local-only decision.
1. Per-render private-network opt-out
For example:
The option should default to
false, with documentation explaining that it must only be enabled when all image sources are trusted.A narrower variation could permit loopback without permitting every private network:
2. Per-image-request allow callback
A callback would let applications make a more targeted decision:
Satori could continue applying its normal protection unless the callback explicitly permits the individual URL.
This could also be expressed as an allowlist:
A callback is more flexible, but either form would let us authorize only the local asset server rather than disabling the checker entirely.
3. Pluggable image fetcher
The initial version of #769 included a custom fetcher, but it was removed because a
typeof fetchsignature would allow caller-controlledRequestand redirect behavior and would not compose safely with hardened fetch libraries.A narrower signature could avoid that concern:
Satori would provide only the URL string, leaving redirect policy and network enforcement entirely to the application-supplied implementation.
Our application already has a hardened server-side fetch implementation that:
Allowing us to provide that implementation would both restore local development and provide stronger production protection than a literal-host check alone.
An image resolver returning data rather than a
Responsecould be another option:Security expectations
We are not requesting that localhost be allowed by default.
Any escape hatch could:
In our case, reduced protection would only be enabled in local development. Production uses public HTTPS asset origins and would continue using SSRF enforcement or our application’s hardened fetcher.