mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-13 21:27:53 +02:00
🔒️(settings) add settings for the embed block
We added 2 settings for the embed block: - FRONTEND_EMBED_BLOCK_ENABLED: to enable or disable the embed block in the editor - FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS: to define a list of allowed origins for the embed block, and the associated sandbox attributes (allow-scripts, allow-same-origin, etc.) This will allow to control the usage of the embed block in the editor, and to restrict the origins that can be embedded.
This commit is contained in:
@@ -80,6 +80,8 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_JS_URL | To add a external js file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_EMBED_BLOCK_ENABLED | Frontend feature flag to allow inserting the "embed" block | true |
|
||||
| FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS | Allowlist mapping each embeddable host to its iframe sandbox, e.g. `{"excalidraw.com": "allow-scripts allow-same-origin"}`. Each key matches only that exact host; prefix with `*.` to also cover its subdomains, or use a bare `*` to allow any host | {"*": "allow-scripts allow-same-origin"} |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LANGFUSE_SECRET_KEY | The Langfuse secret key used by the sdk | None |
|
||||
|
||||
@@ -3087,6 +3087,8 @@ class ConfigView(drf.views.APIView):
|
||||
"CONVERSION_UPLOAD_ENABLED",
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
"FRONTEND_EMBED_BLOCK_ENABLED",
|
||||
"FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
"FRONTEND_JS_URL",
|
||||
"FRONTEND_SILENT_LOGIN_ENABLED",
|
||||
|
||||
@@ -29,6 +29,10 @@ pytestmark = pytest.mark.django_db
|
||||
COLLABORATION_WS_INACTIVITY_TIMEOUT=300,
|
||||
CONVERSION_UPLOAD_ENABLED=False,
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_EMBED_BLOCK_ENABLED=True,
|
||||
FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS={
|
||||
"excalidraw.com": "allow-scripts allow-same-origin",
|
||||
},
|
||||
FRONTEND_JS_URL="http://testjs/",
|
||||
FRONTEND_THEME="test-theme",
|
||||
MEDIA_BASE_URL="http://testserver/",
|
||||
@@ -63,6 +67,10 @@ def test_api_config(is_authenticated):
|
||||
"CONVERSION_UPLOAD_ENABLED": False,
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
"FRONTEND_EMBED_BLOCK_ENABLED": True,
|
||||
"FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS": {
|
||||
"excalidraw.com": "allow-scripts allow-same-origin",
|
||||
},
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
|
||||
"FRONTEND_JS_URL": "http://testjs/",
|
||||
"FRONTEND_SILENT_LOGIN_ENABLED": False,
|
||||
|
||||
@@ -554,6 +554,28 @@ class Base(Configuration):
|
||||
FRONTEND_SILENT_LOGIN_ENABLED = values.BooleanValue(
|
||||
default=False, environ_name="FRONTEND_SILENT_LOGIN_ENABLED", environ_prefix=None
|
||||
)
|
||||
FRONTEND_EMBED_BLOCK_ENABLED = values.BooleanValue(
|
||||
default=True, environ_name="FRONTEND_EMBED_BLOCK_ENABLED", environ_prefix=None
|
||||
)
|
||||
# Allowlist of origins that may be embedded through the "embed" custom block,
|
||||
# mapping each allowed host to the sandbox attribute to apply to its iframe.
|
||||
# A URL whose host is not covered here is refused by the frontend.
|
||||
# Example:
|
||||
# {
|
||||
# "grist.numerique.gouv.fr": "allow-scripts",
|
||||
# "*.numerique.gouv.fr": "allow-scripts allow-same-origin",
|
||||
# }
|
||||
FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS = values.DictValue(
|
||||
default={
|
||||
"*": (
|
||||
"allow-scripts allow-same-origin allow-popups "
|
||||
"allow-popups-to-escape-sandbox allow-forms"
|
||||
)
|
||||
},
|
||||
environ_name="FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
|
||||
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
|
||||
environ_name="THEME_CUSTOMIZATION_FILE_PATH",
|
||||
|
||||
@@ -3,8 +3,18 @@ import path from 'path';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
|
||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import {
|
||||
createDoc,
|
||||
goToGridDoc,
|
||||
overrideConfig,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import {
|
||||
getEditor,
|
||||
openSuggestionMenu,
|
||||
tryFocusEditorContent,
|
||||
writeInEditor,
|
||||
} from './utils-editor';
|
||||
import { updateShareLink } from './utils-share';
|
||||
import {
|
||||
createRootSubPage,
|
||||
@@ -671,7 +681,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page
|
||||
.locator('[data-test="embed-input"]')
|
||||
.fill('http://127.0.0.1:3000/');
|
||||
.fill('http://localhost:3001/');
|
||||
await page.locator('[data-test="embed-input-button"]').click();
|
||||
|
||||
const embedIframe = page
|
||||
@@ -680,9 +690,85 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
// Check src of embed iframe
|
||||
expect(await embedIframe.getAttribute('src')).toMatch(
|
||||
/http:\/\/127.0.0.1:3000\//,
|
||||
/http:\/\/localhost:3001\//,
|
||||
);
|
||||
|
||||
await expect(embedIframe).toHaveAttribute('role', 'presentation');
|
||||
});
|
||||
|
||||
test('it controls the embeds of a web page with configuration', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// Disable the embed block feature to test the configuration
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_EMBED_BLOCK_ENABLED: false,
|
||||
});
|
||||
|
||||
await createDoc(page, 'doc-embed-web-configuration', browserName, 1);
|
||||
|
||||
await tryFocusEditorContent({ page });
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('/');
|
||||
|
||||
await expect(
|
||||
page.locator('.bn-suggestion-menu').getByText('Embed a web page'),
|
||||
).toBeHidden();
|
||||
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS: {
|
||||
'http://localhost:3001': 'allow-scripts',
|
||||
},
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
await openSuggestionMenu({ page, suggestion: 'Embed a web page' });
|
||||
|
||||
const embedBlock = page.locator('div[data-content-type="embed"]').last();
|
||||
|
||||
await expect(embedBlock).toBeVisible();
|
||||
|
||||
// Try with a domain that is not allowed first
|
||||
await page
|
||||
.getByText(/Add embed/)
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page
|
||||
.locator('[data-test="embed-input"]')
|
||||
.fill('http://localhost:3002/');
|
||||
|
||||
await page.locator('[data-test="embed-input-button"]').click();
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
'This domain is not allowed for embedding. Please contact your administrator to have it added to the list of allowed domains.',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await openSuggestionMenu({ page, suggestion: 'Embed a web page' });
|
||||
|
||||
// Now with a valid URL
|
||||
await page
|
||||
.getByText(/Add embed/)
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page
|
||||
.locator('[data-test="embed-input"]')
|
||||
.fill('http://localhost:3001/');
|
||||
await page.locator('[data-test="embed-input-button"]').click();
|
||||
|
||||
const embedIframe = page
|
||||
.locator('.--docs--editor-container iframe.bn-visual-media')
|
||||
.first();
|
||||
|
||||
// Check src of embed iframe
|
||||
expect(await embedIframe.getAttribute('src')).toMatch(
|
||||
/http:\/\/localhost:3001\//,
|
||||
);
|
||||
await expect(embedIframe).toHaveAttribute('sandbox', 'allow-scripts');
|
||||
await expect(embedIframe).toHaveAttribute('role', 'presentation');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,10 @@ export const CONFIG = {
|
||||
CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'],
|
||||
CONVERSION_FILE_MAX_SIZE: 20971520,
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_EMBED_BLOCK_ENABLED: true,
|
||||
FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS: {
|
||||
'*': 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms',
|
||||
},
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_JS_URL: null,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
|
||||
|
||||
@@ -42,6 +42,17 @@ interface ThemeCustomization {
|
||||
waffle?: LaGaufreV2Props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of an allowed embed host to the iframe `sandbox` attribute to apply.
|
||||
* Record<host, sandboxAttribute>
|
||||
* Example:
|
||||
* {
|
||||
* "excalidraw.com": "allow-scripts allow-same-origin",
|
||||
* "www.tldraw.com": "allow-scripts allow-same-origin",
|
||||
* }
|
||||
*/
|
||||
export type EmbedAllowedOrigins = Record<string, string>;
|
||||
|
||||
export interface ConfigResponse {
|
||||
AI_BOT: { name: string; color: string };
|
||||
AI_FEATURE_ENABLED?: boolean;
|
||||
@@ -56,6 +67,8 @@ export interface ConfigResponse {
|
||||
CONVERSION_UPLOAD_ENABLED?: boolean;
|
||||
ENVIRONMENT: string;
|
||||
FRONTEND_CSS_URL?: string;
|
||||
FRONTEND_EMBED_BLOCK_ENABLED?: boolean;
|
||||
FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS?: EmbedAllowedOrigins;
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
|
||||
FRONTEND_JS_URL?: string;
|
||||
FRONTEND_SILENT_LOGIN_ENABLED?: boolean;
|
||||
|
||||
+7
-1
@@ -11,6 +11,8 @@ import {
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConfig } from '@/core';
|
||||
|
||||
import {
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
@@ -45,6 +47,7 @@ export const BlockNoteSuggestionMenu = ({
|
||||
const dictionaryDate = useDictionary();
|
||||
const basicBlocksName = dictionaryDate.slash_menu.page_break.group;
|
||||
const fileBlocksName = dictionaryDate.slash_menu.file.group;
|
||||
const embedEnabled = useConfig().data?.FRONTEND_EMBED_BLOCK_ENABLED ?? false;
|
||||
|
||||
const getInterlinkingMenuItems = useGetInterlinkingMenuItems();
|
||||
|
||||
@@ -57,7 +60,9 @@ export const BlockNoteSuggestionMenu = ({
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
|
||||
getEmbedReactSlashMenuItems(editor, t, fileBlocksName),
|
||||
embedEnabled
|
||||
? getEmbedReactSlashMenuItems(editor, t, fileBlocksName)
|
||||
: [],
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
aiAllowed && getAISlashMenuItems ? getAISlashMenuItems(editor) : [],
|
||||
);
|
||||
@@ -82,6 +87,7 @@ export const BlockNoteSuggestionMenu = ({
|
||||
fileBlocksName,
|
||||
basicBlocksName,
|
||||
aiAllowed,
|
||||
embedEnabled,
|
||||
getInterlinkingMenuItems,
|
||||
]);
|
||||
|
||||
|
||||
+98
-5
@@ -3,9 +3,14 @@
|
||||
* `<iframe>`. The URL is stored in the collaborative document and rendered on
|
||||
* every member's machine, so it is treated as untrusted content.
|
||||
*
|
||||
* ⚠️ To keep it secure, the block enforces two invariants:
|
||||
* ⚠️ To keep it secure, the block enforces three invariants:
|
||||
* 1. The URL must be safe (https, no javascript: or data:).
|
||||
* 2. The URL must be cross-origin (not same-origin with the app).
|
||||
* 3. The URL's host must be explicitly allowlisted server-side
|
||||
* (FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS). Each allowlist entry also carries
|
||||
* the `sandbox` applied to its iframe, so an embed only gets the
|
||||
* capabilities the administrator granted to that origin. A host that is not
|
||||
* listed is refused, and the user is invited to ask an administrator.
|
||||
*
|
||||
* Two layers keep it cross-origin, where the
|
||||
* Same-Origin Policy blocks parent access:
|
||||
@@ -38,6 +43,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
import { EmbedAllowedOrigins, useConfig } from '@/core';
|
||||
import { isSafeUrl } from '@/utils/url';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
@@ -66,6 +72,74 @@ export const isSameOriginUrl = (url: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the `sandbox` to apply for a URL, or `undefined` when the host is not
|
||||
* covered by the configured origins.
|
||||
*
|
||||
* Entry formats (port is always significant):
|
||||
* - `"example.com"` — exact host, default port only
|
||||
* - `"example.com:8080"` — exact host, specific port
|
||||
* - `"*.example.com"` — all subdomains, default port only
|
||||
* - `"*.example.com:8080"` — all subdomains, specific port
|
||||
* - `"*"` — catch-all (any host/port)
|
||||
*
|
||||
* Priority: exact host > longest `"*."` wildcard > `"*"` catch-all.
|
||||
* See `matchEmbedOrigin` tests for concrete examples (`./__tests__/EmbedBlock.test.ts`).
|
||||
*/
|
||||
export const matchEmbedOrigin = (
|
||||
url: string,
|
||||
allowedOrigins: EmbedAllowedOrigins,
|
||||
): string | undefined => {
|
||||
let host: string;
|
||||
try {
|
||||
host = new URL(url, window.location.origin).host.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let bestWildcardMatch: { baseHost: string; sandbox: string } | undefined;
|
||||
let catchAllSandbox: string | undefined;
|
||||
|
||||
for (const [origin, sandbox] of Object.entries(allowedOrigins)) {
|
||||
let allowedHost = origin.trim().toLowerCase();
|
||||
if (allowedHost === '*') {
|
||||
catchAllSandbox = sandbox;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedHost.includes('://')) {
|
||||
try {
|
||||
allowedHost = new URL(allowedHost).host;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
allowedHost = allowedHost.split('/')[0];
|
||||
}
|
||||
|
||||
const isWildcard = allowedHost.startsWith('*.');
|
||||
const baseHost = isWildcard ? allowedHost.slice(2) : allowedHost;
|
||||
if (!baseHost) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isWildcard && host === baseHost) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
if (
|
||||
isWildcard &&
|
||||
host.endsWith(`.${baseHost}`) &&
|
||||
(!bestWildcardMatch ||
|
||||
baseHost.length > bestWildcardMatch.baseHost.length)
|
||||
) {
|
||||
bestWildcardMatch = { baseHost, sandbox };
|
||||
}
|
||||
}
|
||||
|
||||
return bestWildcardMatch?.sandbox ?? catchAllSandbox;
|
||||
};
|
||||
|
||||
type CreateEmbedBlockConfig = BlockConfig<
|
||||
'embed',
|
||||
{
|
||||
@@ -111,13 +185,32 @@ const EmbedBlockComponent = ({ editor, block }: EmbedBlockComponentProps) => {
|
||||
}
|
||||
}, [lang, t]);
|
||||
|
||||
const isInvalidEmbed =
|
||||
!!embedUrl && (!isSafeUrl(embedUrl) || isSameOriginUrl(embedUrl));
|
||||
const conf = useConfig().data;
|
||||
const allowedOrigins = conf?.FRONTEND_EMBED_BLOCK_ALLOWED_ORIGINS ?? {};
|
||||
|
||||
if (isInvalidEmbed) {
|
||||
const isUnsafeEmbed =
|
||||
!!embedUrl && (!isSafeUrl(embedUrl) || isSameOriginUrl(embedUrl));
|
||||
const sandbox =
|
||||
!!embedUrl && !isUnsafeEmbed
|
||||
? matchEmbedOrigin(embedUrl, allowedOrigins)
|
||||
: undefined;
|
||||
const isDisallowedEmbed =
|
||||
!!embedUrl && !isUnsafeEmbed && sandbox === undefined;
|
||||
|
||||
if (isUnsafeEmbed) {
|
||||
return <CustomBlockStatus>{t('Invalid or unsafe URL.')}</CustomBlockStatus>;
|
||||
}
|
||||
|
||||
if (isDisallowedEmbed) {
|
||||
return (
|
||||
<CustomBlockStatus>
|
||||
{t(
|
||||
'This domain is not allowed for embedding. Please contact your administrator to have it added to the list of allowed domains.',
|
||||
)}
|
||||
</CustomBlockStatus>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EmbedBlockStyle />
|
||||
@@ -137,7 +230,7 @@ const EmbedBlockComponent = ({ editor, block }: EmbedBlockComponentProps) => {
|
||||
$height="450px"
|
||||
src={embedUrl}
|
||||
title={block.props.name || t('Embedded content')}
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"
|
||||
sandbox={sandbox ?? ''}
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
contentEditable={false}
|
||||
|
||||
+137
-2
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isSameOriginUrl } from '../EmbedBlock';
|
||||
import { isSameOriginUrl, matchEmbedOrigin } from '../EmbedBlock';
|
||||
|
||||
describe('EmbedBlock', () => {
|
||||
describe('isSameOriginUrl', () => {
|
||||
@@ -24,8 +24,143 @@ describe('EmbedBlock', () => {
|
||||
expect(isSameOriginUrl('https://localhost:9999/')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an unparseable URL (treated as unsafe)', () => {
|
||||
it('returns true for an unparsable URL (treated as unsafe)', () => {
|
||||
expect(isSameOriginUrl('not a valid url %%')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchEmbedOrigin', () => {
|
||||
it('returns the sandbox for an exact host match', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com/page', {
|
||||
'example.com': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('returns undefined when the host is not in the allowlist', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com/', {
|
||||
'other.com': 'allow-scripts',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('matches a wildcard entry for a subdomain', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://docs.numerique.gouv.fr/', {
|
||||
'*.numerique.gouv.fr': 'allow-scripts allow-same-origin',
|
||||
}),
|
||||
).toBe('allow-scripts allow-same-origin');
|
||||
});
|
||||
|
||||
it('does not match the bare base host with a wildcard entry', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://numerique.gouv.fr/', {
|
||||
'*.numerique.gouv.fr': 'allow-scripts',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers an exact host match over a wildcard match', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://grist.numerique.gouv.fr/', {
|
||||
'*.numerique.gouv.fr': 'allow-scripts',
|
||||
'grist.numerique.gouv.fr': 'allow-scripts allow-forms',
|
||||
}),
|
||||
).toBe('allow-scripts allow-forms');
|
||||
});
|
||||
|
||||
it('prefers the longest wildcard match when multiple wildcards match', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://a.b.example.com/', {
|
||||
'*.example.com': 'sandbox-a',
|
||||
'*.b.example.com': 'sandbox-b',
|
||||
}),
|
||||
).toBe('sandbox-b');
|
||||
});
|
||||
|
||||
it('matches the catch-all "*" entry when no other entry matches', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://anything.example.com/', {
|
||||
'*': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('prefers any specific match over the catch-all "*"', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com/', {
|
||||
'*': 'allow-scripts',
|
||||
'example.com': 'allow-scripts allow-forms',
|
||||
}),
|
||||
).toBe('allow-scripts allow-forms');
|
||||
});
|
||||
|
||||
it('accepts origin entries written as full URLs and extracts the hostname', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com/page', {
|
||||
'https://example.com/': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('returns undefined for an unparsable URL', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('not a valid url %%', {
|
||||
'example.com': 'allow-scripts',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for an empty allowlist', () => {
|
||||
expect(matchEmbedOrigin('https://example.com/', {})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does NOT match an exact host entry when the URL has a different port', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com:8080/page', {
|
||||
'example.com': 'allow-scripts',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('matches an exact host entry when the allowlist entry includes the same port', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com:8080/page', {
|
||||
'example.com:8080': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('does NOT match a wildcard entry when the URL has a different port', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://sub.example.com:8443/', {
|
||||
'*.example.com': 'allow-scripts',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('matches a wildcard entry that includes the same port', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://sub.example.com:8443/', {
|
||||
'*.example.com:8443': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('matches the catch-all "*" entry for a URL with a non-standard port', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com:3000/', { '*': 'allow-scripts' }),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
|
||||
it('allows any https URL and returns the default sandbox', () => {
|
||||
expect(
|
||||
matchEmbedOrigin('https://example.com/', {
|
||||
'*': 'allow-scripts',
|
||||
}),
|
||||
).toBe('allow-scripts');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user