Skip to content

Commit 2e97fb5

Browse files
committed
iframe-proxy: respect scoped frame ancestors
1 parent 9164a43 commit 2e97fb5

2 files changed

Lines changed: 21 additions & 9 deletions

File tree

docs/specs/dor-iframe.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ new technique, the proven one.
6767
| Target | Proxy behavior |
6868
| --- | --- |
6969
| **Loopback** (`localhost`/`127.0.0.1`/`[::1]`) http | Full instrument: strip `X-Frame-Options`, drop the page CSP, inject the shim, serve. The user spawned it; framing is the intent. |
70-
| **Remote** http, frameable | Best-effort render with the injected shim (flagged that `dor ab` is the better tool for arbitrary browsing). |
70+
| **Remote** http, frameable | Best-effort render with the injected shim (flagged that `dor ab` is the better tool for arbitrary browsing). A CSP `frame-ancestors *` is considered frameable; scoped sources such as `https://*.example.com` are restrictive. |
7171
| **Remote** http, refuses framing | **Never force-framed.** Serve a Dormouse error page with a one-click hint to `dor ab open <url>`. |
7272
| **Unreachable** (conn refused, DNS, non-2xx) | Serve a Dormouse error page ("is the dev server running?"). |
7373
| **`https://`** | Deferred. The panel reports `scheme` and points at `dor ab`. |
@@ -205,8 +205,10 @@ The same fences as the stream relay: loopback-only bind both sides; a per-surfac
205205
grant served by a dedicated **single-upstream** server (no open forwarder); the
206206
injected shim is fixed and Dormouse-owned (no user script ever reaches the page);
207207
header-stripping never force-frames a third-party site (a refusing remote is
208-
diverted to an error page, not stripped). **SSRF:** the proxy fetches a
209-
user-supplied URL, so it refuses link-local / cloud-metadata ranges
208+
diverted to an error page, not stripped). For CSP, only a standalone
209+
`frame-ancestors *` is permissive; wildcard host patterns remain restrictive.
210+
**SSRF:** the proxy fetches a user-supplied URL, so it refuses link-local /
211+
cloud-metadata ranges
210212
(`169.254.0.0/16`, `fe80::/10`) and trusts other rangesthe trust boundary is the
211213
user's own `dor iframe <url>`.
212214

vscode-ext/src/iframe-proxy-host.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,25 @@ function instrumentHtml(body: string): string {
272272
}
273273

274274
// A remote refuses framing if it sends any X-Frame-Options or a CSP
275-
// frame-ancestors that is not the permissive `*`. Conservative on purpose: when
276-
// in doubt we divert to an error page rather than show a guaranteed-blank frame.
275+
// frame-ancestors that is not the permissive standalone `*`. Conservative on
276+
// purpose: when in doubt we divert to an error page rather than show a
277+
// guaranteed-blank frame.
277278
function refusesFraming(headers: http.IncomingHttpHeaders): boolean {
278279
if (headers['x-frame-options']) return true;
279280
const csp = headers['content-security-policy'];
280-
const cspText = Array.isArray(csp) ? csp.join(';') : (csp ?? '');
281-
const match = /frame-ancestors([^;]*)/i.exec(cspText);
282-
if (!match) return false;
283-
return !/\*/.test(match[1]);
281+
const policies = Array.isArray(csp) ? csp : csp ? [csp] : [];
282+
return policies.some((policy) => hasRestrictiveFrameAncestors(policy));
283+
}
284+
285+
function hasRestrictiveFrameAncestors(policy: string): boolean {
286+
const directives = policy.split(';');
287+
for (const directive of directives) {
288+
const parts = directive.trim().split(/\s+/).filter(Boolean);
289+
if (parts.length === 0 || parts[0].toLowerCase() !== 'frame-ancestors') continue;
290+
const sources = parts.slice(1);
291+
if (!sources.includes('*')) return true;
292+
}
293+
return false;
284294
}
285295

286296
// --- WebSocket upgrade passthrough (dev-server HMR, openvscode-server) -------

0 commit comments

Comments
 (0)