diff --git a/config/scripts/electron-builder-mobile-web-fixture.mjs b/config/scripts/electron-builder-mobile-web-fixture.mjs index 50c8ee3b0db..cb9116015ce 100644 --- a/config/scripts/electron-builder-mobile-web-fixture.mjs +++ b/config/scripts/electron-builder-mobile-web-fixture.mjs @@ -1,6 +1,7 @@ import { createHash } from 'node:crypto' import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' +import { MOBILE_WEB_MARKDOWN_EDITOR_PATH } from '../../mobile/src/components/markdown-editor-document' import { MOBILE_WEB_MERMAID_FRAME_PATH, buildMobileWebMermaidFrameDocument @@ -28,27 +29,35 @@ export async function createPackagedCliResourceFixture(resourcesDir) { export async function createMobileWebResourceFixture(resourcesDir) { const root = join(resourcesDir, 'mobile-web') - const script = Buffer.from('globalThis.__orcaPackagedMobileWeb=true', 'utf8') - const scriptHash = sha256(script) - const scriptPath = `assets/${scriptHash}.js` + const scripts = ['globalThis.__orcaPackagedMobileWeb=true', 'void 0', 'void 1'].map((source) => { + const bytes = Buffer.from(source, 'utf8') + const hash = sha256(bytes) + return { bytes, hash, path: `assets/${hash}.js` } + }) + const [entryScript, mermaidScript, editorScript] = scripts const document = Buffer.from( - ``, + ``, 'utf8' ) const mermaidFrame = Buffer.from( buildMobileWebMermaidFrameDocument({ - theme: { background: 'black', primary: 'gray', text: 'white', line: 'silver' } + theme: { background: 'black', primary: 'gray', text: 'white', line: 'silver' }, + script: { src: `./${mermaidScript.path}` } }), 'utf8' ) + const markdownEditor = Buffer.from( + ``, + 'utf8' + ) const assets = [ - { - path: scriptPath, - sha256: scriptHash, - byteLength: script.byteLength, + ...scripts.map((entry) => ({ + path: entry.path, + sha256: entry.hash, + byteLength: entry.bytes.byteLength, contentType: 'text/javascript; charset=utf-8', role: 'script' - }, + })), { path: 'index.html', sha256: sha256(document), @@ -56,6 +65,13 @@ export async function createMobileWebResourceFixture(resourcesDir) { contentType: 'text/html; charset=utf-8', role: 'document' }, + { + path: MOBILE_WEB_MARKDOWN_EDITOR_PATH, + sha256: sha256(markdownEditor), + byteLength: markdownEditor.byteLength, + contentType: 'text/html; charset=utf-8', + role: 'document' + }, { path: MOBILE_WEB_MERMAID_FRAME_PATH, sha256: sha256(mermaidFrame), @@ -64,6 +80,7 @@ export async function createMobileWebResourceFixture(resourcesDir) { role: 'document' } ] + assets.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) const seed = { schemaVersion: MOBILE_WEB_MANIFEST_SCHEMA_VERSION, buildId: '0'.repeat(64), @@ -74,8 +91,11 @@ export async function createMobileWebResourceFixture(resourcesDir) { } const manifest = { ...seed, buildId: sha256(serializeMobileWebManifestForBuildId(seed)) } await mkdir(join(root, 'assets'), { recursive: true }) - await writeFile(join(root, scriptPath), script) + for (const entry of scripts) { + await writeFile(join(root, entry.path), entry.bytes) + } await writeFile(join(root, 'index.html'), document) + await writeFile(join(root, MOBILE_WEB_MARKDOWN_EDITOR_PATH), markdownEditor) await writeFile(join(root, MOBILE_WEB_MERMAID_FRAME_PATH), mermaidFrame) await writeFile(join(root, 'manifest.json'), JSON.stringify(manifest)) } diff --git a/config/scripts/package-mobile-web-rnw.mjs b/config/scripts/package-mobile-web-rnw.mjs index 66b953e9801..948a1402a07 100644 --- a/config/scripts/package-mobile-web-rnw.mjs +++ b/config/scripts/package-mobile-web-rnw.mjs @@ -9,12 +9,21 @@ import { } from '../../src/shared/mobile-web/manifest-contract.ts' import { MOBILE_WEB_MERMAID_FRAME_PATH, + MOBILE_WEB_MERMAID_FRAME_SCRIPT, buildMobileWebMermaidFrameDocument } from '../../mobile/src/components/pr-sidebar/mermaid-frame-document.ts' import { colors } from '../../mobile/src/theme/mobile-theme.ts' +import { registerTypeScriptExtensionlessResolver } from './typescript-extensionless-resolver.mjs' import { splitMobileWebRnwScript } from './mobile-web-rnw-script-chunks.mjs' import { assertMobileWebRnwExecutablePolicy } from './mobile-web-rnw-executable-policy.mjs' +registerTypeScriptExtensionlessResolver() +const { + MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT, + MOBILE_WEB_MARKDOWN_EDITOR_PATH, + buildMobileRichMarkdownEditorHtml +} = await import('../../mobile/src/components/mobile-rich-markdown-editor-html.ts') + const args = parseArgs(process.argv.slice(2)) const inputRoot = path.resolve(args.input ?? 'out/mobile-web-rnw-export') const outputRoot = path.resolve(args.output ?? 'out/mobile-web-rnw') @@ -67,15 +76,21 @@ const scriptPaths = splitMobileWebRnwScript(script).map((chunk) => { const document = mobileWebDocument({ scriptPaths, stylePath }) const documentBytes = Buffer.from(document) packaged.set('index.html', documentBytes) +// Frame scripts ship as ordinary content-addressed assets so the native CSP never pins a hash. const mermaidFrame = buildMobileWebMermaidFrameDocument({ theme: { background: colors.bgRaised, primary: colors.bgPanel, text: colors.textPrimary, line: colors.textSecondary - } + }, + script: { src: `./${packageScript(packaged, MOBILE_WEB_MERMAID_FRAME_SCRIPT)}` } }) packaged.set(MOBILE_WEB_MERMAID_FRAME_PATH, Buffer.from(mermaidFrame)) +const markdownEditor = buildMobileRichMarkdownEditorHtml({ + src: `./${packageScript(packaged, MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT)}` +}) +packaged.set(MOBILE_WEB_MARKDOWN_EDITOR_PATH, Buffer.from(markdownEditor)) for (const [assetPath, bytes] of packaged) { await mkdir(path.dirname(path.join(outputRoot, assetPath)), { recursive: true }) @@ -166,6 +181,13 @@ function replaceReferences(source, replacements) { return output } +function packageScript(packaged, source) { + const bytes = Buffer.from(`${source}\n`) + const assetPath = contentAddressedPath(bytes, '.js') + packaged.set(assetPath, bytes) + return assetPath +} + function mobileWebDocument({ scriptPaths, stylePath }) { return ` diff --git a/config/scripts/package-mobile-web-rnw.test.ts b/config/scripts/package-mobile-web-rnw.test.ts index 2427f308ab8..0241d1196fa 100644 --- a/config/scripts/package-mobile-web-rnw.test.ts +++ b/config/scripts/package-mobile-web-rnw.test.ts @@ -5,11 +5,9 @@ import { promisify } from 'node:util' import { afterEach, describe, expect, it } from 'vitest' import { MobileWebPackageAssets } from '../../src/main/runtime/rpc/mobile-web-package-assets' import { MOBILE_WEB_PACKAGE_BRIDGE_RANGE } from '../../src/shared/mobile-web/bridge-limits' +import { MOBILE_WEB_MARKDOWN_EDITOR_PATH } from '../../mobile/src/components/markdown-editor-document' import { MobileWebManifestSchema } from '../../src/shared/mobile-web/manifest-contract' -import { - MOBILE_WEB_MERMAID_FRAME_PATH, - MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH -} from '../../mobile/src/components/pr-sidebar/mermaid-frame-document' +import { MOBILE_WEB_MERMAID_FRAME_PATH } from '../../mobile/src/components/pr-sidebar/mermaid-frame-document' const execFileAsync = promisify(execFile) const temporaryRoots: string[] = [] @@ -73,6 +71,10 @@ describe('RNW mobile web packager', () => { ) const document = await readFile(path.join(output, 'index.html'), 'utf8') const mermaidFrame = await readFile(path.join(output, MOBILE_WEB_MERMAID_FRAME_PATH), 'utf8') + const markdownEditor = await readFile( + path.join(output, MOBILE_WEB_MARKDOWN_EDITOR_PATH), + 'utf8' + ) expect(script).not.toMatch(/\beval\s*\(|\bnew\s+Function\s*\(/) expect(script).not.toContain('/assets/icon.hash.png') expect(script).toMatch(/\.\/assets\/[a-f0-9]{64}\.png/) @@ -83,11 +85,16 @@ describe('RNW mobile web packager', () => { expect(document).not.toContain('Content-Security-Policy') expect(document).toContain('maximum-scale=1,user-scalable=no') expect(document).toContain('viewport-fit=cover') - expect(mermaidFrame).toContain(`script-src ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH} blob:`) - expect(mermaidFrame).toContain("frame-ancestors 'self'") + // No served document may carry an inline script, or a native CSP would have to pin its hash. + for (const served of [document, mermaidFrame, markdownEditor]) { + expect(served).not.toMatch(/]*\bsrc=)/) + expect(served).not.toContain('sha256-') + expect(served).not.toContain('Content-Security-Policy') + expect(served).toMatch(/') + expect(packaged).not.toMatch(/]*\bsrc=)/) + expect(packaged).not.toContain('Content-Security-Policy') + expect(packaged).not.toContain('sha256-') + expect(packaged).toContain(MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_SELECTOR) + }) + it('renders the Markdown XSS corpus as inert content and rejects active URL schemes', () => { const html = runtimeMarkdownToHtml( [ diff --git a/mobile/src/components/mobile-rich-markdown-editor-html.ts b/mobile/src/components/mobile-rich-markdown-editor-html.ts index c11ac26f8d6..5f20b472a72 100644 --- a/mobile/src/components/mobile-rich-markdown-editor-html.ts +++ b/mobile/src/components/mobile-rich-markdown-editor-html.ts @@ -1,11 +1,14 @@ -import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from './markdown-editor-csp' +import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from './markdown-editor-document' import { colors } from '../theme/mobile-theme' import { MOBILE_RICH_MARKDOWN_EDITOR_DOCUMENT_BODY } from './mobile-rich-markdown-editor-document-body' import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT } from './mobile-rich-markdown-editor-script' export { escapeInjectedJavaScriptString } from './mobile-rich-markdown-editor-script-string' export { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT } from './mobile-rich-markdown-editor-script' -export { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from './markdown-editor-csp' +export { + MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH, + MOBILE_WEB_MARKDOWN_EDITOR_PATH +} from './markdown-editor-document' // Why: `https:` keeps the remote markdown images main rendered; plaintext `http:` stays blocked. const MOBILE_RICH_MARKDOWN_EDITOR_FRAME_CSP = `` diff --git a/mobile/src/components/mobile-rich-markdown-editor-web-source.test.ts b/mobile/src/components/mobile-rich-markdown-editor-web-source.test.ts index 2a1ea8c9891..9243aada85b 100644 --- a/mobile/src/components/mobile-rich-markdown-editor-web-source.test.ts +++ b/mobile/src/components/mobile-rich-markdown-editor-web-source.test.ts @@ -30,9 +30,9 @@ describe('mobile rich markdown editor web source', () => { expect(webEditor).not.toContain("label: 'Body'") }) - it('runs the exact editor document in an isolated data frame', () => { - expect(webEditor).toContain('buildMobileRichMarkdownEditorHtml({ inline: true })') - expect(webEditor).toContain('data:text/html;charset=utf-8,') + it('runs the packaged editor document in an isolated frame', () => { + expect(webEditor).toContain('src={`/${MOBILE_WEB_MARKDOWN_EDITOR_PATH}`}') + expect(webEditor).not.toContain('data:text/html;charset=utf-8,') expect(webEditor).toContain('sandbox="allow-scripts"') expect(webEditor).toContain('name={frameToken}') expect(webEditor).not.toContain('allow-same-origin') diff --git a/mobile/src/components/pr-sidebar/mermaid-diagram-document.test.ts b/mobile/src/components/pr-sidebar/mermaid-diagram-document.test.ts index db3caa2a532..714e3bdd869 100644 --- a/mobile/src/components/pr-sidebar/mermaid-diagram-document.test.ts +++ b/mobile/src/components/pr-sidebar/mermaid-diagram-document.test.ts @@ -4,7 +4,6 @@ import { gunzipSync } from 'node:zlib' import { describe, expect, it } from 'vitest' import { MOBILE_WEB_MERMAID_FRAME_PATH, - MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH, buildMobileWebMermaidFrameDocument } from './mermaid-frame-document' import { @@ -63,13 +62,16 @@ describe('Mermaid diagram document', () => { primary: '#1a1a1a', text: '#e0e0e0', line: '#888888' - } + }, + script: { src: './assets/mermaid.js' } }) expect(Buffer.byteLength(document)).toBeLessThan(16 * 1024) expect(document).not.toContain(MERMAID_WEBVIEW_ENGINE_GZIP_BASE64) - expect(document).toContain(`script-src ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH} blob:`) - expect(document).toContain("frame-ancestors 'self'") + expect(document).toContain('') + expect(document).not.toMatch(/]*\bsrc=)/) + expect(document).not.toContain('Content-Security-Policy') + expect(document).not.toContain('sha256-') expect(document).not.toContain('graph TD; A-->B') expect(document).not.toContain('frame-token') expect(MERMAID_DIAGRAM_SCRIPT.indexOf('window.parent !== window')).toBeLessThan( diff --git a/mobile/src/components/pr-sidebar/mermaid-diagram-document.ts b/mobile/src/components/pr-sidebar/mermaid-diagram-document.ts index 6ed97fda4c8..23a9e22b9a5 100644 --- a/mobile/src/components/pr-sidebar/mermaid-diagram-document.ts +++ b/mobile/src/components/pr-sidebar/mermaid-diagram-document.ts @@ -4,6 +4,7 @@ import { MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH, buildMobileWebMermaidFrameDocument } from './mermaid-frame-document' +import { mobileWebEmbeddedFrameCsp } from '../../mobile-web/embedded-frame-csp' import { colors } from '../../theme/mobile-theme' import { MERMAID_WEBVIEW_ENGINE_CSP_HASH, @@ -23,14 +24,16 @@ const theme = { } export function buildMermaidDiagramDocument(source: string, token = ''): string { - const document = buildMobileWebMermaidFrameDocument({ + return buildMobileWebMermaidFrameDocument({ theme, + // The in-app WebView inlines both the engine and the frame script, so both are hashed. + script: { + inlineCsp: mobileWebEmbeddedFrameCsp( + `${MERMAID_WEBVIEW_ENGINE_CSP_HASH} ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH}` + ) + }, embeddedEngine: MERMAID_WEBVIEW_ENGINE_GZIP_BASE64, encodedSource: Buffer.from(source, 'utf8').toString('base64'), encodedToken: Buffer.from(token, 'utf8').toString('base64') }) - return document.replace( - `script-src ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH} blob:`, - `script-src ${MERMAID_WEBVIEW_ENGINE_CSP_HASH} ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH}` - ) } diff --git a/mobile/src/components/pr-sidebar/mermaid-frame-document.ts b/mobile/src/components/pr-sidebar/mermaid-frame-document.ts index 0c9eb4f8286..d25a7b2fc8a 100644 --- a/mobile/src/components/pr-sidebar/mermaid-frame-document.ts +++ b/mobile/src/components/pr-sidebar/mermaid-frame-document.ts @@ -177,29 +177,6 @@ export const MOBILE_WEB_MERMAID_FRAME_SCRIPT = String.raw`(function () { export const MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH = "'sha256-JHwlo5V7HtwqexHUhXguW04dF71kAVlQOX1QdtyCkjg='" -export function mobileWebMermaidFrameCspDirectives() { - return [ - "default-src 'none'", - `script-src ${MOBILE_WEB_MERMAID_FRAME_SCRIPT_CSP_HASH} blob:`, - "style-src 'unsafe-inline'", - 'img-src data:', - "font-src 'none'", - "connect-src 'none'", - "media-src 'none'", - "object-src 'none'", - "frame-src 'none'", - "child-src 'none'", - "worker-src 'none'", - "base-uri 'none'", - "form-action 'none'", - "frame-ancestors 'self'" - ] as const -} - -export function mobileWebMermaidFrameCsp(): string { - return mobileWebMermaidFrameCspDirectives().join('; ') -} - type MermaidFrameTheme = { background: string primary: string @@ -207,8 +184,16 @@ type MermaidFrameTheme = { line: string } +/** + * The packaged frame loads its script from the package and takes its policy from the native + * response header, which is the only place that knows the per-session origin. The in-app document + * has no server, so it carries its own meta policy over an inline script. + */ +type MermaidFrameScript = { src: string } | { inlineCsp: string } + type MermaidFrameDocumentOptions = { theme: MermaidFrameTheme + script: MermaidFrameScript embeddedEngine?: string encodedSource?: string encodedToken?: string @@ -216,16 +201,24 @@ type MermaidFrameDocumentOptions = { export function buildMobileWebMermaidFrameDocument({ theme, + script, embeddedEngine = '', encodedSource = '', encodedToken = '' }: MermaidFrameDocumentOptions): string { + const policy = + 'src' in script + ? '' + : `\n ` + const scriptElement = + 'src' in script + ? `` + : `` return ` - - + ${policy} @@ -233,7 +226,7 @@ export function buildMobileWebMermaidFrameDocument({
- + ${scriptElement} ` } diff --git a/mobile/src/mobile-web/embedded-frame-csp.ts b/mobile/src/mobile-web/embedded-frame-csp.ts new file mode 100644 index 00000000000..6d52c6a6509 --- /dev/null +++ b/mobile/src/mobile-web/embedded-frame-csp.ts @@ -0,0 +1,30 @@ +/** + * Policy the native shell serves for the package's embedded frame documents (mermaid, markdown + * editor). The in-app WebView mermaid document reuses the shape over its own inline script. + * + * `scriptSources` is spelled out by the caller because the frames are sandboxed: WebKit resolves + * `'self'` against the frame's opaque origin, so a served frame has to name the package origin to + * load its own script. Chromium accepts `'self'` there; WebKit does not. + */ +export function mobileWebEmbeddedFrameCspDirectives(scriptSources: string) { + return [ + "default-src 'none'", + `script-src ${scriptSources}`, + "style-src 'unsafe-inline'", + 'img-src data:', + "font-src 'none'", + "connect-src 'none'", + "media-src 'none'", + "object-src 'none'", + "frame-src 'none'", + "child-src 'none'", + "worker-src 'none'", + "base-uri 'none'", + "form-action 'none'", + "frame-ancestors 'self'" + ] as const +} + +export function mobileWebEmbeddedFrameCsp(scriptSources: string): string { + return mobileWebEmbeddedFrameCspDirectives(scriptSources).join('; ') +} diff --git a/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts b/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts index 79b4e9e8689..e40467b198c 100644 --- a/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts +++ b/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts @@ -1,8 +1,7 @@ import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' import { MOBILE_WEB_BRIDGE_MAX_MESSAGE_BYTES } from '../../../src/shared/mobile-web/bridge-contract' -import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from '../components/markdown-editor-csp' -import { mobileWebMermaidFrameCspDirectives } from '../components/pr-sidebar/mermaid-frame-document' +import { mobileWebEmbeddedFrameCspDirectives } from './embedded-frame-csp' const iosSource = readFileSync( new URL('../../packages/expo-mobile-web-shell/ios/MobileWebShellView.swift', import.meta.url), @@ -199,13 +198,16 @@ describe('mobile web native bridge transport', () => { expect(nativeCspDirectives(iosSource, 'mobileWebCsp')).toEqual( nativeCspDirectives(androidSource, 'MOBILE_WEB_CSP') ) - expect(nativeCspDirectives(iosSource, 'mobileWebMermaidFrameCsp')).toEqual( - mobileWebMermaidFrameCspDirectives() + // The frame policies interpolate the per-session package origin, so the mirror keeps the + // native interpolation token in the script source it compares against. + expect(nativeCspDirectives(iosSource, 'func mobileWebEmbeddedFrameCsp')).toEqual( + mobileWebEmbeddedFrameCspDirectives('\\(origin) blob:') ) - expect(nativeCspDirectives(androidSource, 'MOBILE_WEB_MERMAID_FRAME_CSP')).toEqual( - mobileWebMermaidFrameCspDirectives() + expect(nativeCspDirectives(androidSource, 'fun mobileWebEmbeddedFrameCsp')).toEqual( + mobileWebEmbeddedFrameCspDirectives('$origin blob:') ) for (const source of [iosSource, androidSource]) { + expect(source).not.toContain('sha256-') expect(source).toContain('"frame-src \'self\' data:"') expect(source).toContain('"child-src \'self\' data:"') expect(source).toContain('"connect-src \'none\'"') @@ -214,13 +216,9 @@ describe('mobile web native bridge transport', () => { expect(source).toContain('"form-action \'none\'"') expect(source).not.toContain("\"script-src 'self' 'unsafe-inline'\"") } - expect(iosSource).toContain( - `"script-src 'self' ${MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH}"` - ) + expect(iosSource).toContain('"script-src \'self\'"') expect(iosSource).toContain("\"style-src 'self' 'unsafe-inline'\"") - expect(androidSource).toContain( - `"script-src 'self' ${MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH}"` - ) + expect(androidSource).toContain('"script-src \'self\'"') expect(androidSource).toContain("\"style-src 'self' 'unsafe-inline'\"") expect(androidSource).toContain('"font-src \'self\'"') expect(androidSource).toContain('"img-src \'self\' data: blob:"') @@ -251,10 +249,15 @@ function nativeBlockerScript(source: string, declaration: string): string { } function nativeCspDirectives(source: string, declaration: string): string[] { - const kotlinStart = source.indexOf(`${declaration} = listOf(`) - const swiftStart = source.indexOf(`${declaration} = [`) - const opening = kotlinStart !== -1 ? kotlinStart : swiftStart - const closing = source.indexOf(kotlinStart !== -1 ? ').joinToString' : '].joined', opening) + const declared = source.indexOf(declaration) + if (declared === -1) { + return [] + } + const kotlinStart = source.indexOf('listOf(', declared) + const swiftStart = source.indexOf('[', declared) + const kotlin = kotlinStart !== -1 && (swiftStart === -1 || kotlinStart < swiftStart) + const opening = kotlin ? kotlinStart : swiftStart + const closing = source.indexOf(kotlin ? ').joinToString' : '].joined', opening) if (opening === -1 || closing === -1) { return [] } diff --git a/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts b/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts index ff26c639eb4..54f26d1c49b 100644 --- a/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts +++ b/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts @@ -1,6 +1,5 @@ import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' -import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from '../components/markdown-editor-csp' const iosShellViewSource = readFileSync( new URL('../../packages/expo-mobile-web-shell/ios/MobileWebShellView.swift', import.meta.url), @@ -30,9 +29,7 @@ describe('mobile web shell root route', () => { expect(iosShellViewSource).toContain('url.path == "/"') expect(iosShellViewSource).not.toContain('url.path == "/index.html"') expect(iosShellViewSource).toContain("\"style-src 'self' 'unsafe-inline'\"") - expect(iosShellViewSource).toContain( - `"script-src 'self' ${MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH}"` - ) + expect(iosShellViewSource).toContain('"script-src \'self\'"') expect(iosShellViewSource).not.toContain("\"script-src 'self' 'unsafe-inline'\"") }) diff --git a/src/shared/mobile-web/manifest-contract.ts b/src/shared/mobile-web/manifest-contract.ts index 8d15498ca8c..ce1b3b033db 100644 --- a/src/shared/mobile-web/manifest-contract.ts +++ b/src/shared/mobile-web/manifest-contract.ts @@ -8,7 +8,10 @@ export const MOBILE_WEB_MAX_ASSET_COUNT = 256 export const MOBILE_WEB_MAX_PATH_CHARS = 240 export const MOBILE_WEB_MAX_BRIDGE_VERSION = 65_535 export const MOBILE_WEB_ENTRYPOINT_PATH = 'index.html' -export const MOBILE_WEB_EMBEDDED_DOCUMENT_PATHS = ['mermaid-frame.html'] as const +export const MOBILE_WEB_EMBEDDED_DOCUMENT_PATHS = [ + 'markdown-editor.html', + 'mermaid-frame.html' +] as const const SHA256_PATTERN = /^[a-f0-9]{64}$/ const SAFE_PATH_PATTERN = /^[A-Za-z0-9._/-]+$/