Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions apps/demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[<scheme>://]<host>[:<port>]`, and an omitted part matches anything:

- **Host** — either an exact hostname or a `*.<suffix>` 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
Comment thread
mpreyskurantov marked this conversation as resolved.

Auto-fix lint errors:
Expand Down
3 changes: 1 addition & 2 deletions apps/demos/utils/server/demo-render-signal.runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Comment thread
Copilot marked this conversation as resolved.

const PATTERN = /^(?:([a-z][a-z0-9+.-]*):\/\/)?([^/:]+)(?::(\d+))?$/i;
Expand Down
59 changes: 39 additions & 20 deletions apps/demos/utils/tests/server/demo-render-signal.runtime.test.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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();
Expand All @@ -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');
});
Comment thread
Copilot marked this conversation as resolved.

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) => {
Expand All @@ -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');
Comment thread
vorobey marked this conversation as resolved.
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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});

Expand Down
Loading