diff --git a/apps/demos/README.md b/apps/demos/README.md index c307e742ac9f..07b8c7706398 100644 --- a/apps/demos/README.md +++ b/apps/demos/README.md @@ -28,48 +28,6 @@ You can pass a port as an extra argument. That is useful when you need to switch Angular, React, and Vue demos are bundled on demand when you open a page. jQuery demos load `dx.all.js` from `devextreme-dist`. -### Demo render signal - -Angular, React, and Vue demos are not bundled from their own entry point directly. `utils/server/demo-render-signal.js` generates a shim that becomes the bundle's entry point; the shim waits for `themes.initialized()` from `devextreme/ui/themes`, then imports the demo's entry (`index.tsx` / `index.ts` / `app/app.component.ts`), so nothing mounts before the theme CSS is applied. - -Once the demo has rendered, the shim posts one message to the embedding page: - -```js -window.parent.postMessage({ type: 'demo-rendered' }, targetOrigin); -``` - -#### Allowed embedding origins - -`targetOrigin` is never `'*'` — the runtime resolves the embedder's origin and posts only when it is trusted: - -1. Not framed, or framed by a page on the demo's own origin — posts to the own origin. This covers local development and the visual tests, so `localhost` needs no configuration. -2. Framed cross-origin — the origin comes from `location.ancestorOrigins[0]`, falling back to the origin of `document.referrer` (Firefox has no `ancestorOrigins`), and must match the allowlist. -3. Origin not derivable (for example, an embedder sending `Referrer-Policy: no-referrer` on Firefox) or not on the allowlist — nothing is posted, and the demo logs a warning to the console. - -The allowlist defaults to the sandboxes that embed the demos: - -| Entry | Covers | -| --- | --- | -| `*.devexpress.com` | `js.devexpress.com`, `js-stage.devexpress.com`, `az-jsserver.corp.devexpress.com` | -| `js.devexpress.devx` | the internal dev host | -| `localhost` | the local site on any port, when it frames demos served from another port | - -An entry is `[://][:]`, and an omitted part matches anything: - -- **Host** — either an exact hostname or a `*.` subdomain wildcard. The wildcard requires a dot before the suffix, so `*.devexpress.com` accepts `js.devexpress.com` and `az-jsserver.corp.devexpress.com` but rejects the `devexpress.com` apex and `evil-devexpress.com`. -- **Scheme** — omit it to accept both HTTP and HTTPS; write `https://js.devexpress.com` to accept only HTTPS. -- **Port** — omit it to accept any port; write `localhost:44332` to accept only that one. A pinned port must match the origin's explicit port, so `:443` will not match `https://host`. - -Anything that is not a bare origin — a trailing slash or a path, for example — never matches and is silently ignored. - -Override the list at build time with a comma-separated `DEMO_PARENT_ORIGINS`: - -``` -DEMO_PARENT_ORIGINS='https://js.devexpress.com,https://staging.example:8443' node utils/server/csp-bundle.js --framework=React -``` - -The list is baked into every demo bundle, so adding a sandbox origin means rebuilding the demos. - ### Before Commiting Changes Auto-fix lint errors: diff --git a/apps/demos/utils/server/demo-render-signal.runtime.js b/apps/demos/utils/server/demo-render-signal.runtime.js index cb843746e4e7..4fc043e37a52 100644 --- a/apps/demos/utils/server/demo-render-signal.runtime.js +++ b/apps/demos/utils/server/demo-render-signal.runtime.js @@ -6,9 +6,8 @@ export const ROOT_SELECTOR = '#app, demo-app'; export const RENDER_TIMEOUT_MS = 10000; export const DEFAULT_ALLOWED_ORIGINS = [ - '*.devexpress.com', - 'js.devexpress.devx', 'localhost', + 'https://js.devexpress.com', ]; const PATTERN = /^(?:([a-z][a-z0-9+.-]*):\/\/)?([^/:]+)(?::(\d+))?$/i; diff --git a/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js b/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js index 1ee30a1a1e42..e2f94564b2eb 100644 --- a/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js +++ b/apps/demos/utils/tests/server/demo-render-signal.runtime.test.js @@ -1,5 +1,6 @@ -const OWN_ORIGIN = 'http://localhost:8080'; -const ALLOWED_PARENT = 'https://js.devexpress.com'; +const OWN_ORIGIN = 'https://demos.test'; +const ALLOWED_PARENT_HOST = 'js.devexpress.com'; +const ALLOWED_PARENT = `https://${ALLOWED_PARENT_HOST}`; let messages; let root; @@ -107,7 +108,7 @@ describe('signal', () => { expect(messages).toHaveLength(0); expect(warnings).toHaveLength(1); - expect(warnings[0]).toContain('*.devexpress.com'); + expect(warnings[0]).toContain(ALLOWED_PARENT_HOST); }); test('gives up for good after a dropped message', () => { @@ -130,12 +131,9 @@ describe('resolveTargetOrigin', () => { }); test.each([ - 'https://az-jsserver.corp.devexpress.com', - 'https://js-stage.devexpress.com', 'https://js.devexpress.com', - 'https://js.devexpress.devx', - 'http://localhost:44332', 'http://localhost:8080', + 'http://localhost:3000', ])('allows the %s sandbox', (origin) => { embedIn(origin); const { resolveTargetOrigin } = loadRuntime(); @@ -145,15 +143,16 @@ describe('resolveTargetOrigin', () => { test('allows any scheme for an entry that omits one', () => { embedIn('http://js.devexpress.com'); - const { resolveTargetOrigin } = loadRuntime(); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['js.devexpress.com']); expect(resolveTargetOrigin()).toBe('http://js.devexpress.com'); }); test.each([ - ['the apex domain, which no sandbox uses', 'https://devexpress.com'], - ['a host that only ends with the wildcard suffix', 'https://evil-devexpress.com'], - ['a host shorter than the wildcard suffix', 'https://dx.com'], + ['the apex of an allowed host', 'https://devexpress.com'], + ['a host that merely ends with an allowed one', 'https://evil-js.devexpress.com'], ['a host that merely starts with an allowed one', 'https://localhost.evil.example'], ['an opaque origin', 'null'], ])('rejects %s', (_, origin) => { @@ -176,18 +175,40 @@ describe('resolveTargetOrigin', () => { embedIn('http://localhost:9999'); const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); - setAllowedOrigins(['localhost:44332']); + setAllowedOrigins(['localhost:8080']); expect(resolveTargetOrigin()).toBeNull(); }); test('matches a pinned port', () => { - embedIn('http://localhost:44332'); + embedIn('http://localhost:8080'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['http://localhost:8080']); + + expect(resolveTargetOrigin()).toBe('http://localhost:8080'); + }); + + test('allows a subdomain of a wildcard entry', () => { + embedIn('https://demos.example.com'); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); + + setAllowedOrigins(['*.example.com']); + + expect(resolveTargetOrigin()).toBe('https://demos.example.com'); + }); + + test.each([ + ['the apex domain', 'https://example.com'], + ['a host that only ends with the wildcard suffix', 'https://evil-example.com'], + ['a host shorter than the wildcard suffix', 'https://ex.com'], + ])('rejects %s for a wildcard entry', (_, origin) => { + embedIn(origin); const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); - setAllowedOrigins(['http://localhost:44332']); + setAllowedOrigins(['*.example.com']); - expect(resolveTargetOrigin()).toBe('http://localhost:44332'); + expect(resolveTargetOrigin()).toBeNull(); }); test('ignores an entry that is not a bare origin', () => { @@ -200,11 +221,10 @@ describe('resolveTargetOrigin', () => { }); test('allows an embedder on the demo own origin, allowlisted or not', () => { - global.window.location.origin = 'https://unlisted.example'; - embedIn('https://unlisted.example'); + embedIn(OWN_ORIGIN); const { resolveTargetOrigin } = loadRuntime(); - expect(resolveTargetOrigin()).toBe('https://unlisted.example'); + expect(resolveTargetOrigin()).toBe(OWN_ORIGIN); }); test('targets the own origin when the demo is not framed', () => { @@ -258,13 +278,12 @@ describe('setAllowedOrigins', () => { }); test('keeps the default allowlist when the build injects nothing', () => { - const { setAllowedOrigins, resolveTargetOrigin, DEFAULT_ALLOWED_ORIGINS } = loadRuntime(); + const { setAllowedOrigins, resolveTargetOrigin } = loadRuntime(); setAllowedOrigins(null); setAllowedOrigins([]); expect(resolveTargetOrigin()).toBe(ALLOWED_PARENT); - expect(DEFAULT_ALLOWED_ORIGINS).toContain('*.devexpress.com'); }); });