The Isolation Boundary Needs Consent
Cross-origin isolation is easy to write down and hard to ship:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
That looks like one switch. It is not. It is two contracts plus a dependency audit.
Cross-Origin-Opener-Policy asks whether this top-level document should share
an opener-connected browsing context group with cross-origin windows.
Cross-Origin-Embedder-Policy asks whether this document is allowed to load
cross-origin resources that did not explicitly opt in. CORP is one way a
resource owner gives that opt-in.
The useful mental model is:
COOP: who can share my opener/window group?
COEP: may I embed resources that did not consent?
CORP/CORS: did this particular resource consent?
crossOriginIsolated: did the browser grant the resulting capability?
That last line is why this is worth the trouble. The HTML Standard defines
self.crossOriginIsolated as the global’s cross-origin isolated capability,
and says the value depends on the COOP and COEP response headers and the
cross-origin-isolated feature.1
The Header Pair Is Asymmetric
COOP is about top-level browsing contexts. In the HTML Standard, an opener
policy value can force a newly navigated document into a new top-level browsing
context group. same-origin also makes cross-origin auxiliary contexts appear
closed to the opener unless they are same-origin and have a matching policy.2
The special value authors actually get for isolation is not directly settable:
same-origin-plus-COEP
It results from combining:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
or:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
The spec says same-origin-plus-COEP comes from COOP: same-origin together
with a COEP value compatible with cross-origin isolation.3
That COEP-compatible set is require-corp or credentialless.4
This is the first deployment trap:
COOP only -> opener relationship changes, but no isolated capability
COEP only -> subresource contract applies, but no isolated capability
COOP+COEP -> isolated group and API capability, assuming the feature is allowed
The second trap is parsing. COEP is a Structured Field token. The HTML processing
model defaults to unsafe-none when a header cannot be parsed as a token,
including accidental combined lists such as require-corp, require-corp.5
With security headers, duplicate emission is not harmless housekeeping.
COEP Makes Dependencies Prove Themselves
COEP has three values:
| COEP value | Effect |
|---|---|
unsafe-none |
Cross-origin resources can be fetched without explicit permission through CORS or CORP. |
require-corp |
Cross-origin resources need explicit permission through CORS or CORP. |
credentialless |
Cross-origin no-CORS resources are fetched without credentials; other credentialed requests still need explicit permission. |
That table is mostly HTML’s definition in operational clothing.4
require-corp is strict. If your page has a third-party image, classic script,
iframe, worker, or opaque no-CORS fetch that does not opt in, the browser can
block it. For iframes and dedicated workers there is another inheritance rule:
when the parent has a COEP value compatible with cross-origin isolation, the
child context’s policy also has to be compatible, or the navigation/worker
initialization fails.6
credentialless is a different bargain. For cross-origin no-CORS subresources,
the browser omits credentials; in exchange, an explicit CORP header is not
required. That can rescue static public assets from a CDN you do not control.
It does not rescue credentialed CORS API calls, and it does not make iframe
children inherit COEP.
CORP Is The Resource Owner’s Sentence
CORP lives on the response being embedded:
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Cross-Origin-Resource-Policy: cross-origin
Fetch’s cross-origin resource policy check reads that header and applies the
resource owner’s declared boundary. If the policy is same-origin, the response
is allowed only to a same-origin requester. If it is same-site, the requester
has to be same-site, with a secure-transport caveat for HTTPS responses. If it
is cross-origin, the resource is loadable across origins.7
The important inversion:
COEP is the embedder saying "I will not load unconsented resources."
CORP is the resource saying "Here is who may embed me."
That means CORP can protect a resource even when the embedding page did not set
COEP. A bank logo served with Cross-Origin-Resource-Policy: same-origin is not
silently available as an opaque cross-origin image to arbitrary pages in the
model below.
Lab: Which Dependency Breaks Isolation?
The lab below models a top-level page, a small dependency list, an optional cross-origin popup, and the COOP/COEP/CORP checks that decide whether the page gets the isolated capability and which dependencies load.
The default run is deliberately painful: COOP + require-corp on a marketing-style page with a hero image, analytics script, ad iframe, and identity popup. The executable model gives:
| Default metric | Value |
|---|---|
crossOriginIsolated |
yes |
| Loaded resources | 1 |
| Blocked resources | 3 |
| Reports | 0 |
| Credential-stripped requests | 0 |
| Audit | passes |
The page gets the isolated capability, but most of the page does not load. That is not a contradiction. Isolation is not a compatibility promise; it is an enforced boundary.
Try report-only rollout with the same dependency set. Nothing is blocked, but the lab records reports. Try COOP + credentialless with credentialless CDN. Public no-CORS image/script requests load with credentials stripped, while a credentialed private API still fails because it did not opt in with CORS or CORP.
crossOriginIsolated is true. Some dependencies are blocked.
Reproducing The Model
The simulator exports a Node-testable API:
node - <<'NODE'
const lab = require("./assets/js/cross-origin-isolation-lab.js");
const EXPECTED_CRITICAL_CHECKS = 17;
const EXPECTED_CASES = 40;
const EXPECTED_CHECKS = 137;
const EXPECTED_SUMMARY_KEYS = [
"total",
"loaded",
"blocked",
"reported",
"stripped",
"openerSevered",
"groupIsolated",
"crossOriginIsolated",
"sharedMemory"
];
const EXPECTED_DEFAULT_SUMMARY_SHAPE = [
"total:4",
"loaded:1",
"blocked:3",
"reported:0",
"stripped:0",
"openerSevered:1",
"groupIsolated:1",
"crossOriginIsolated:1",
"sharedMemory:1"
];
const EXPECTED_CREDENTIALLESS_SUMMARY_SHAPE = [
"total:4",
"loaded:3",
"blocked:1",
"reported:0",
"stripped:2",
"openerSevered:0",
"groupIsolated:1",
"crossOriginIsolated:1",
"sharedMemory:1"
];
const EXPECTED_TOTALS = {
criticalChecks: EXPECTED_CRITICAL_CHECKS,
rows: EXPECTED_CASES,
checked: EXPECTED_CHECKS,
passedChecks: EXPECTED_CHECKS,
totalChecks: EXPECTED_CHECKS
};
const EXPECTED_BY_DEPLOYMENT_SHAPE = [
"coepOnly:5/15:10/8/0/0:0/0/0/0",
"coopOnly:5/15:16/2/0/0:1/0/0/0",
"credentialless:5/15:14/4/0/7:1/5/5/5",
"duplicateCoep:5/15:16/2/0/0:1/0/0/0",
"openWeb:5/15:16/2/0/0:0/0/0/0",
"permissionBlocked:5/15:10/8/0/0:1/5/0/0",
"reportOnly:5/15:16/2/9/0:0/0/0/0",
"requireCorp:5/15:10/8/0/0:1/5/5/5"
];
const EXPECTED_BY_RESOURCE_SHAPE = [
"cleanWasm:8/24:32/0/0/0:0/3/2/2",
"credentiallessCdn:8/24:22/10/3/2:0/3/2/2",
"defensiveCorp:8/24:16/8/1/1:0/3/2/2",
"sameSiteAssets:8/24:16/8/1/2:0/3/2/2",
"thirdPartyPage:8/24:22/10/4/2:5/3/2/2"
];
const EXPECTED_CRITICAL_SHAPE = [
"COEP alone does not isolate:1/1",
"COOP alone does not isolate:1/1",
"COOP plus credentialless isolates clean app:1/1",
"COOP plus require-corp isolates clean app:1/1",
"COOP same-origin severs cross-origin popup opener:1/1",
"CORP same-origin protects resource even without COEP:1/1",
"credentialless still blocks credentialed CORS without permission:1/1",
"credentialless strips no-CORS credentials:1/1",
"invalid combined COEP fails open:1/1",
"open CORP workload loads non-protected resources:1/1",
"open defaults keep popup opener:1/1",
"permission policy can deny gated APIs:1/1",
"report-only does not block resources:1/1",
"report-only records violations:1/1",
"require-corp blocks missing CORP no-CORS resources:1/1",
"require-corp blocks unopted third-party resources:1/1",
"same-site CORP secure mismatch blocks one resource:1/1"
];
function sameJson(actual, expected) {
return JSON.stringify(actual) === JSON.stringify(expected);
}
function summaryShape(summary) {
return EXPECTED_SUMMARY_KEYS.map((key) => `${key}:${summary[key]}`);
}
function groupShape(row, key) {
return [
row[key],
`${row.cases}/${row.totalChecks}`,
`${row.loaded}/${row.blocked}/${row.reported}/${row.stripped}`,
`${row.openerSevered}/${row.groupIsolated}/` +
`${row.crossOriginIsolated}/${row.sharedMemory}`
].join(":");
}
const defaultSummary = lab.evaluate({}).summary;
const credentiallessSummary = lab.evaluate({
deployment: "credentialless",
resources: "credentiallessCdn"
}).summary;
console.log(defaultSummary);
console.log(credentiallessSummary);
const audit = lab.auditCrossOriginIsolationLab();
console.table(audit.byDeployment);
console.table(audit.byResourceSet);
console.table(audit.criticalChecks);
const auditShape = {
defaultSummary: summaryShape(defaultSummary),
credentiallessSummary: summaryShape(credentiallessSummary),
totals: {
criticalChecks: audit.criticalTotal,
rows: audit.cases.length,
checked: audit.checked,
passedChecks: audit.passedChecks,
totalChecks: audit.totalChecks
},
byDeployment: audit.byDeployment.map((row) => groupShape(row, "deployment")),
byResourceSet: audit.byResourceSet.map((row) => groupShape(row, "resources")),
criticalChecks: audit.criticalChecks.map(
(row) => `${row.check}:${row.passedChecks}/${row.totalChecks}`
).sort()
};
const shapeErrors = [
sameJson(auditShape.defaultSummary, EXPECTED_DEFAULT_SUMMARY_SHAPE)
? null
: "defaultSummary",
sameJson(auditShape.credentiallessSummary, EXPECTED_CREDENTIALLESS_SUMMARY_SHAPE)
? null
: "credentiallessSummary",
sameJson(auditShape.totals, EXPECTED_TOTALS) ? null : "totals",
sameJson(auditShape.byDeployment, EXPECTED_BY_DEPLOYMENT_SHAPE)
? null
: "byDeployment",
sameJson(auditShape.byResourceSet, EXPECTED_BY_RESOURCE_SHAPE)
? null
: "byResourceSet",
sameJson(auditShape.criticalChecks, EXPECTED_CRITICAL_SHAPE)
? null
: "criticalChecks"
].filter(Boolean);
if (shapeErrors.length) {
throw new Error(
`audit grid drifted in ${shapeErrors.join(", ")}:\n` +
JSON.stringify(auditShape, null, 2)
);
}
const failedCriticalChecks = audit.criticalChecks.filter(
(row) => !row.passed || row.passedChecks !== row.totalChecks
);
const failedCases = audit.cases.filter(
(row) => !row.passed || row.passedChecks !== row.totalChecks
);
if (!audit.ok || failedCriticalChecks.length || failedCases.length || audit.passedChecks !== audit.totalChecks) {
throw new Error(JSON.stringify({
errors: audit.errors,
failedCriticalChecks,
failedCases,
shapeErrors,
auditShape
}, null, 2));
}
console.log(`${audit.passedChecks}/${audit.totalChecks} checks passed`);
NODE
Default summary:
{
"total": 4,
"loaded": 1,
"blocked": 3,
"reported": 0,
"stripped": 0,
"openerSevered": 1,
"groupIsolated": 1,
"crossOriginIsolated": 1,
"sharedMemory": 1
}
Credentialless CDN summary:
{
"total": 4,
"loaded": 3,
"blocked": 1,
"reported": 0,
"stripped": 2,
"openerSevered": 0,
"groupIsolated": 1,
"crossOriginIsolated": 1,
"sharedMemory": 1
}
The audit currently runs 17 named critical checks plus 120 generated invariant checks over a 40-row deployment/resource grid (137 checks total). Among the invariants:
- COOP alone and COEP alone do not set
crossOriginIsolated; COOP: same-originplusCOEP: require-corporcredentiallesscan set it;- permission policy can still deny the API capability;
- invalid combined COEP header values fail open to
unsafe-none; - report-only rollout records would-block events without enforcing them;
require-corpblocks unopted cross-origin subresources;credentiallessstrips credentials from cross-origin no-CORS subresources;- credentialed private CORS requests still need explicit permission;
CORP: same-origincan protect a resource even without COEP on the embedder.
What I Would Put In A Review
The review is not “set COOP and COEP.” It is a dependency inventory:
| Question | Evidence |
|---|---|
| Which page needs isolated APIs? | A concrete use of SharedArrayBuffer, high-precision timing, memory measurement, or another gated API. |
| Is the top-level document isolated? | COOP: same-origin, compatible COEP, secure context, and self.crossOriginIsolated === true. |
Which popups need postMessage back to the opener? |
OAuth, payments, support chat, and admin consoles are checked before enforcing COOP. |
| Which dependencies are opaque no-CORS loads? | Images, classic scripts, iframes, workers, and CDN assets are listed with their response headers. |
| Which resource owner opts in? | Cross-origin dependencies use CORS or an appropriate CORP value. |
Is credentialless acceptable? |
The dependency is public and should not receive cookies or other ambient credentials. |
| Are report-only headers deployed first? | COOP/COEP reports are collected before enforcement on a large or third-party-heavy page. |
| Are duplicate headers impossible? | CDN, app server, and framework middleware cannot combine COEP into an invalid list. |
Cross-origin isolation is not a badge you add to a document. It is a claim that every resource sharing the address-space neighborhood either belongs there, explicitly consented to be there, or arrives without credentials.
-
WHATWG HTML,
crossOriginIsolated. The getter returns the relevant settings object’s cross-origin isolated capability, and the API description ties it to COOP, COEP, and thecross-origin-isolatedfeature. ↩ -
WHATWG HTML, Cross-origin opener policies. ↩
-
WHATWG HTML,
same-origin-plus-COEP, the value resulting fromCOOP: same-originplus a COEP value compatible with cross-origin isolation. ↩ -
WHATWG HTML, Cross-origin embedder policies. ↩ ↩2
-
WHATWG HTML, COEP header parsing, including the default to
unsafe-nonefor unparseable token lists. ↩ -
WHATWG HTML, Embedder policy checks, including nested navigation and dedicated worker checks. ↩
-
WHATWG Fetch, Cross-origin resource policy check. ↩