diff --git a/packages/rum/src/domain/deflate/deflateWorker.ts b/packages/rum/src/domain/deflate/deflateWorker.ts index 76cfa22a12..b98ad2ba1f 100644 --- a/packages/rum/src/domain/deflate/deflateWorker.ts +++ b/packages/rum/src/domain/deflate/deflateWorker.ts @@ -42,7 +42,11 @@ type DeflateWorkerState = export type CreateDeflateWorker = typeof createDeflateWorker function createDeflateWorker(configuration: RumConfiguration): DeflateWorker { - return new Worker(configuration.workerUrl || URL.createObjectURL(new Blob([__BUILD_ENV__WORKER_STRING__]))) + return new Worker( + getTrustedTypePolicy().createScriptURL( + configuration.workerUrl || URL.createObjectURL(new Blob([__BUILD_ENV__WORKER_STRING__])) + ) + ) } let state: DeflateWorkerState = { status: DeflateWorkerStatus.Nil } @@ -155,3 +159,26 @@ function onError(configuration: RumConfiguration, source: string, error: unknown }) } } + +// Trusted Types are not yet in TypeScript lib +interface WindowWithTrustedTypes { + trustedTypes?: { + createPolicy(name: string, policy: TrustedTypesPolicy): TrustedTypesPolicy + } +} + +interface TrustedTypesPolicy { + createScriptURL(url: string): string +} + +let trustedTypesPolicyCache: TrustedTypesPolicy +function getTrustedTypePolicy(): TrustedTypesPolicy { + if (!trustedTypesPolicyCache) { + trustedTypesPolicyCache = { createScriptURL: (url) => url } + const trustedTypes = (window as WindowWithTrustedTypes).trustedTypes + if (trustedTypes) { + trustedTypesPolicyCache = trustedTypes.createPolicy('datadog-worker', trustedTypesPolicyCache) + } + } + return trustedTypesPolicyCache +} diff --git a/test/e2e/lib/framework/serverApps/mock.ts b/test/e2e/lib/framework/serverApps/mock.ts index c7993678d0..ba1add414a 100644 --- a/test/e2e/lib/framework/serverApps/mock.ts +++ b/test/e2e/lib/framework/serverApps/mock.ts @@ -1,7 +1,6 @@ -import type { ServerResponse } from 'http' import * as url from 'url' import cors from 'cors' -import express from 'express' +import express, { type Response } from 'express' import { getSdkBundlePath, getTestAppBundlePath } from '../sdkBuilds' import type { MockServerApp, Servers } from '../httpServers' import { DEV_SERVER_BASE_URL } from '../../helpers/playwright' @@ -39,7 +38,7 @@ export function createMockServerApp(servers: Servers, setup: string): MockServer generateLargeResponse(res, chunkText) }) - function generateLargeResponse(res: ServerResponse, chunkText: string) { + function generateLargeResponse(res: Response, chunkText: string) { let bytesWritten = 0 let timeoutId: NodeJS.Timeout @@ -93,26 +92,23 @@ export function createMockServerApp(servers: Servers, setup: string): MockServer }) app.get('/', (_req, res) => { - res.header( - 'Content-Security-Policy', - [ - `connect-src ${servers.intake.url} ${servers.base.url} ${servers.crossOrigin.url}`, - "script-src 'self' 'unsafe-inline'", - 'worker-src blob:', - ].join(';') - ) + addCspHeader(servers, res) res.send(setup) res.end() }) app.get('/no-blob-worker-csp', (_req, res) => { - res.header( - 'Content-Security-Policy', - [ - `connect-src ${servers.intake.url} ${servers.base.url} ${servers.crossOrigin.url}`, - "script-src 'self' 'unsafe-inline'", - ].join(';') - ) + addCspHeader(servers, res, { allowBlobWorker: false }) + res.send(setup) + res.end() + }) + + app.get('/trusted-types-csp', (req, res) => { + const policies = ['datadog-chunks', 'datadog-worker'] + if (typeof req.query['extra-policy'] === 'string') { + policies.push(req.query['extra-policy']) + } + addCspHeader(servers, res, { useTrustedTypes: true }) res.send(setup) res.end() }) @@ -149,7 +145,7 @@ export function createMockServerApp(servers: Servers, setup: string): MockServer // We fetch and pipe the file content instead of redirecting to avoid creating different behavior between CI and local dev // This way both environments serve the files from the same origin with the same CSP rules -function forwardToDevServer(originalUrl: string, res: ServerResponse) { +function forwardToDevServer(originalUrl: string, res: Response) { const url = `${DEV_SERVER_BASE_URL}${originalUrl}` fetch(url) @@ -170,3 +166,25 @@ function forwardToDevServer(originalUrl: string, res: ServerResponse) { }) .catch(() => console.error(`Error fetching ${url}, did you run 'yarn dev'?`)) } + +function addCspHeader( + servers: Servers, + res: Response, + { allowBlobWorker = true, useTrustedTypes = false }: { allowBlobWorker?: boolean; useTrustedTypes?: boolean } = {} +) { + const directives = [ + // Needed to send requests to various servers + `connect-src ${servers.intake.url} ${servers.base.url} ${servers.crossOrigin.url}`, + // Needed to load scripts from the same origin, and executing inline scripts (SDK setups and + // page.evaluate) + "script-src 'self' 'unsafe-inline'", + ] + if (allowBlobWorker) { + directives.push("worker-src 'self' blob:") + } + if (useTrustedTypes) { + directives.push("require-trusted-types-for 'script'") + directives.push('trusted-types datadog-chunks datadog-worker') + } + res.header('Content-Security-Policy', directives.join(';')) +} diff --git a/test/e2e/scenario/transport.scenario.ts b/test/e2e/scenario/transport.scenario.ts index 6b77fb0b63..b8d2ef28ef 100644 --- a/test/e2e/scenario/transport.scenario.ts +++ b/test/e2e/scenario/transport.scenario.ts @@ -51,5 +51,31 @@ test.describe('transport', () => { expect(cspDocLog, "'CSP doc' log").toBeTruthy() }) }) + + createTest('workerUrl initialization parameter') + .withRum({ compressIntakeRequests: true, workerUrl: '/worker.js' }) + .withBasePath('/no-blob-worker-csp') + .run(async ({ intakeRegistry, flushEvents }) => { + await flushEvents() + expect(intakeRegistry.rumRequests.length).toBeGreaterThan(0) + }) + + test.describe('Trusted Types CSP', () => { + createTest('supports Trusted Types CSP') + .withRum({ compressIntakeRequests: true }) + .withBasePath('/trusted-types-csp') + .run(async ({ flushEvents, intakeRegistry }) => { + await flushEvents() + expect(intakeRegistry.rumRequests.length).toBeGreaterThan(0) + }) + + createTest('supports Trusted Types CSP with workerUrl') + .withRum({ compressIntakeRequests: true, workerUrl: '/worker.js' }) + .withBasePath('/trusted-types-csp') + .run(async ({ intakeRegistry, flushEvents }) => { + await flushEvents() + expect(intakeRegistry.rumRequests.length).toBeGreaterThan(0) + }) + }) }) }) diff --git a/webpack.base.js b/webpack.base.js index a5dac270c6..13daab16c9 100644 --- a/webpack.base.js +++ b/webpack.base.js @@ -19,6 +19,9 @@ module.exports = ({ entry, mode, filename, types, keepBuildEnvVariables, plugins : // Include a content hash in chunk names in production. `chunks/[name]-[contenthash]-${filename}`, path: path.resolve('./bundle'), + trustedTypes: { + policyName: 'datadog-chunks', + }, }, target: ['web', 'es2018'], devtool: false,