From efc595f042be934f02be725d82b54a57513fa2fc Mon Sep 17 00:00:00 2001 From: wille Date: Wed, 31 Jul 2024 19:22:16 +0200 Subject: [PATCH] Support CORS --- README.md | 11 +++----- src/reporting-endpoint.ts | 57 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9625f67..3948f5f 100644 --- a/README.md +++ b/README.md @@ -91,14 +91,6 @@ app.listen(8080); > [!NOTE] > The policy headers must be set before the reportingEndpointHeader middleware so the middleware is able to append the reporter to the policy headers. -> *** -> If the reporting endpoint is on another origin, you need to setup CORS -> ```ts -> import cors from 'cors'; -> const corsMiddleware = cors(); -> app.options('/reporting-endpoint', cors()); -> app.post('/reporting-endpoint', cors(), ...); -> ``` ### Response with a `Reporting-Endpoints` header created and reporter setup on the Policy headers ``` @@ -133,6 +125,9 @@ Hello World! - [`reportingEndpoint`](./src/reporting-endpoint.ts) - [`setupReportingHeaders`](./src/setup-headers.ts) +> [!NOTE] +> Set the `allowedOrigins` option on your reporting endpoint to allow cross origin reports. + ## Resources - [Permissions-Policy reporting](https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md) diff --git a/src/reporting-endpoint.ts b/src/reporting-endpoint.ts index a4d92c9..1065c0c 100644 --- a/src/reporting-endpoint.ts +++ b/src/reporting-endpoint.ts @@ -40,6 +40,15 @@ export interface ReportingEndpointConfig { * Debug mode */ debug?: boolean; + + /** + * Set this field to enable CORS for reports sent cross origin to other domains. + * A special value '*' can be set to allow any domain to send reports to your endpoint. + * + * @example 'https://example.com' + * @example /https:\/\/(.*)\.example.com$/ + */ + allowedOrigins?: string | RegExp | (string | RegExp)[]; } function filterReport( @@ -69,8 +78,23 @@ function filterReport( return true; } +function isOriginAllowed( + origin: string, + allowedOrigin: ReportingEndpointConfig['allowedOrigins'] +): boolean { + if (Array.isArray(allowedOrigin)) { + return allowedOrigin.some((o) => isOriginAllowed(origin, o)); + } else if (allowedOrigin instanceof RegExp) { + return allowedOrigin.test(origin); + } else if (typeof allowedOrigin === 'string') { + return allowedOrigin === origin; + } + + return false; +} + function createReportingEndpoint(config: ReportingEndpointConfig) { - const { onReport, onValidationError } = config; + const { onReport, onValidationError, allowedOrigins } = config; if (config.debug) { debug.enable('reporting-api:*'); @@ -103,11 +127,38 @@ function createReportingEndpoint(config: ReportingEndpointConfig) { } } - return (req: Request, res: Response, next: NextFunction) => { - if (req.method !== 'POST') { + return (req: Request, res: Response) => { + if (req.method !== 'POST' && req.method !== 'OPTIONS') { return res.sendStatus(405); } + // If cross origin reports are allowed, setup CORS on both OPTIONS and POST. + if (allowedOrigins) { + const originHeader = req.headers.origin; + + if (config.allowedOrigins === '*') { + res.setHeader('Access-Control-Allow-Origin', '*'); + } else if ( + originHeader && + isOriginAllowed(originHeader, allowedOrigins) + ) { + res.setHeader('Access-Control-Allow-Origin', originHeader); + } + + // Since reports are sent with a Content-Type header MIME type that is not considered 'simple' (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) + // we will always get a preflight request + if (req.method === 'OPTIONS') { + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Access-Control-Allow-Methods', 'POST'); + + // Capped at 7200 in Chrome + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age#delta-seconds + res.setHeader('Access-Control-Max-Age', '7200'); + + return res.sendStatus(200); + } + } + const version = typeof req.query.version === 'string' ? req.query.version