Summary
The Webstudio builder serves three unauthenticated proxy routes, GET /cgi/image/<name>, GET /cgi/video/<name> and GET /cgi/asset/<name>. When the RESIZE_ORIGIN environment variable is not set (the default for a self-hosted deployment; the hosted SaaS sets it and is not affected), and <name> is an absolute URL, the loader returns fetch(name) directly to the caller. There is no scheme allowlist, no private-IP/loopback/metadata block, and no authentication. The only guard on the image/asset routes is a Referer host check that is trivially bypassed (omit the Referer header, or set it to the target host, since it is a client-controlled header); the video route has no referer check at all. As a result, any unauthenticated client can use a self-hosted Webstudio instance as a full-read SSRF proxy to reach cloud metadata services and internal-only hosts and read the responses.
Details
apps/builder/app/routes/cgi.image.$.ts:
// Allow direct image access, and from the same origin
const refererRawUrl = request.headers.get("referer");
const refererUrl = refererRawUrl === null ? url : new URL(refererRawUrl); // no referer -> uses url (passes)
if (refererUrl.host !== url.host) {
throw new Response("Forbidden", { status: 403 });
}
if (env.RESIZE_ORIGIN !== undefined) {
... // hosted path: fetch RESIZE_ORIGIN + loader href (safe)
return responseWHeaders;
}
// support absolute urls locally
if (URL.canParse(name)) {
return fetch(name); // <-- SSRF: arbitrary URL, response returned to caller
}
The Referer check fails open: when the header is absent refererUrl is set to the request URL itself, so refererUrl.host === url.host and the check passes; and because Referer is attacker-controlled, it can simply be set to the instance host. So the check stops naive cross-site <img> embedding from a browser but not a direct attacker. The fetch(name) branch is reached whenever RESIZE_ORIGIN is undefined, which is the normal state of a self-hosted instance that has not wired up a Cloudflare-compatible image-resize origin.
apps/builder/app/routes/cgi.asset.$.ts is identical (referer check, then if (URL.canParse(name)) return fetch(name)), and apps/builder/app/routes/cgi.video.$.ts is worse, it has no referer check at all:
if (env.RESIZE_ORIGIN !== undefined) { ... }
if (URL.canParse(name)) {
return fetch(name); // <-- SSRF, no referer guard
}
None of the three routes import any authentication/session helper; they are public loaders. name is decodePathFragment(url.pathname.slice("/cgi/image/".length)), i.e. everything after the prefix, so a full URL placed there is fetched verbatim. The fetched Response is returned to the client, so this is a full-read SSRF (the attacker sees the response body), not a blind one.
PoC
(available upon request, tested against current main - commit f52545a)
Impact
On a self-hosted Webstudio instance without RESIZE_ORIGIN configured, an unauthenticated attacker turns the server into a full-read SSRF proxy: read cloud metadata (IAM credentials, tokens), reach internal-only services and admin panels not exposed to the internet, and port-scan the internal network, with the responses returned verbatim. Confidentiality impact is high (credential/secret disclosure from internal endpoints). The hosted webstudio.is SaaS, which sets RESIZE_ORIGIN, is not affected. Fix: never fetch(name) on an unvalidated URL; restrict the absolute-URL branch to an allowlist of asset hosts (or remove it), block private/loopback/link-local/metadata IP ranges after DNS resolution (and re-check on redirect), restrict the scheme to http(s), and do not rely on the spoofable Referer header as an access control.
Summary
The Webstudio builder serves three unauthenticated proxy routes,
GET /cgi/image/<name>,GET /cgi/video/<name>andGET /cgi/asset/<name>. When theRESIZE_ORIGINenvironment variable is not set (the default for a self-hosted deployment; the hosted SaaS sets it and is not affected), and<name>is an absolute URL, the loader returnsfetch(name)directly to the caller. There is no scheme allowlist, no private-IP/loopback/metadata block, and no authentication. The only guard on the image/asset routes is aRefererhost check that is trivially bypassed (omit theRefererheader, or set it to the target host, since it is a client-controlled header); the video route has no referer check at all. As a result, any unauthenticated client can use a self-hosted Webstudio instance as a full-read SSRF proxy to reach cloud metadata services and internal-only hosts and read the responses.Details
apps/builder/app/routes/cgi.image.$.ts:The
Referercheck fails open: when the header is absentrefererUrlis set to the request URL itself, sorefererUrl.host === url.hostand the check passes; and becauseRefereris attacker-controlled, it can simply be set to the instance host. So the check stops naive cross-site<img>embedding from a browser but not a direct attacker. Thefetch(name)branch is reached wheneverRESIZE_ORIGINis undefined, which is the normal state of a self-hosted instance that has not wired up a Cloudflare-compatible image-resize origin.apps/builder/app/routes/cgi.asset.$.tsis identical (referer check, thenif (URL.canParse(name)) return fetch(name)), andapps/builder/app/routes/cgi.video.$.tsis worse, it has no referer check at all:None of the three routes import any authentication/session helper; they are public loaders.
nameisdecodePathFragment(url.pathname.slice("/cgi/image/".length)), i.e. everything after the prefix, so a full URL placed there is fetched verbatim. The fetchedResponseis returned to the client, so this is a full-read SSRF (the attacker sees the response body), not a blind one.PoC
(available upon request, tested against current main - commit f52545a)
Impact
On a self-hosted Webstudio instance without
RESIZE_ORIGINconfigured, an unauthenticated attacker turns the server into a full-read SSRF proxy: read cloud metadata (IAM credentials, tokens), reach internal-only services and admin panels not exposed to the internet, and port-scan the internal network, with the responses returned verbatim. Confidentiality impact is high (credential/secret disclosure from internal endpoints). The hosted webstudio.is SaaS, which setsRESIZE_ORIGIN, is not affected. Fix: neverfetch(name)on an unvalidated URL; restrict the absolute-URL branch to an allowlist of asset hosts (or remove it), block private/loopback/link-local/metadata IP ranges after DNS resolution (and re-check on redirect), restrict the scheme to http(s), and do not rely on the spoofableRefererheader as an access control.