|
1 | | -[](https://www.npmjs.com/package/reporting-api) |
2 | | - |
3 | 1 | # reporting-api |
4 | 2 |
|
5 | | -This package provides a middleware for Express.js that automatically configures the [Reporting API](https://w3c.github.io/reporting/) on existing policy headers and an endpoint to help you collect your own reports using the Reporting API. |
| 3 | +[](https://www.npmjs.com/package/reporting-api) |
| 4 | +[](https://github.com/wille/reporting-api/blob/master/LICENSE) |
6 | 5 |
|
7 | | -Automatically sets up reporting for the following headers and features supporting the Reporting API |
8 | | -- [`Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) |
9 | | -- [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) |
10 | | -- [`Permissions-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) |
11 | | -- [`Permissions-Policy-Report-Only`](https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md |
12 | | -) |
13 | | -- [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) (COOP) |
14 | | -- [`Cross-Origin-Opener-Policy-Report-Only`](https://github.com/camillelamy/explainers/blob/main/coop_reporting.md) |
15 | | -- [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) (COEP) |
16 | | -- [`Cross-Origin-Embedder-Policy-Report-Only`](https://gist.github.com/yutakahirano/f14f15bd1595e1e913b0870649000470) |
17 | | -- [`NEL` (Network Error Logging)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging) |
18 | | -- [Deprecation Reports](https://wicg.github.io/deprecation-reporting/) |
19 | | -- [Intervention Reports](https://wicg.github.io/intervention-reporting/) |
20 | | -- [Crash Reports](https://wicg.github.io/crash-reporting/) |
| 6 | +Express.js middleware for the [Reporting API](https://w3c.github.io/reporting/). Automatically wires up `report-to` / `report-uri` on your existing policy headers and gives you a ready-made endpoint to collect violation, deprecation, crash, and network error reports. |
21 | 7 |
|
22 | | -Supports "CSP Level 2 Reports" in browsers browsers not supporting the Reporting API. |
| 8 | +## Supported headers and report types |
23 | 9 |
|
24 | | -## Core concepts |
| 10 | +| Header | Shorthand | |
| 11 | +|--------|-----------| |
| 12 | +| [`Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) | CSP | |
| 13 | +| [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) | | |
| 14 | +| [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) | COOP | |
| 15 | +| [`Cross-Origin-Opener-Policy-Report-Only`](https://github.com/camillelamy/explainers/blob/main/coop_reporting.md) | | |
| 16 | +| [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) | COEP | |
| 17 | +| [`Cross-Origin-Embedder-Policy-Report-Only`](https://gist.github.com/yutakahirano/f14f15bd1595e1e913b0870649000470) | | |
| 18 | +| [`Permissions-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) | | |
| 19 | +| [`Permissions-Policy-Report-Only`](https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md) | | |
| 20 | +| [`NEL` (Network Error Logging)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging) | NEL | |
25 | 21 |
|
26 | | -Retrofitting a policy on a large website is hard to get right first. The solution is to use `-Report-Only` policies that will not enforce them and break your website. These headers and their enforcing equivalents supports reporting, which makes all policy violations gets sent to you so you can adjust your policies to not break functionality. |
| 22 | +Plus [Deprecation](https://wicg.github.io/deprecation-reporting/), [Intervention](https://wicg.github.io/intervention-reporting/), and [Crash](https://wicg.github.io/crash-reporting/) reports. |
27 | 23 |
|
28 | | -### Setup a reporting endpoint and setup reporters on your policy headers |
| 24 | +Backwards-compatible with CSP Level 2 `report-uri` for browsers that don't yet support the Reporting API. |
29 | 25 |
|
| 26 | +## Install |
| 27 | + |
| 28 | +```bash |
| 29 | +npm install reporting-api |
30 | 30 | ``` |
31 | | -$ npm install reporting-api |
32 | | -``` |
| 31 | + |
| 32 | +Peer dependencies: `express`, `zod`, `debug`. |
| 33 | + |
| 34 | +## Quick start |
33 | 35 |
|
34 | 36 | ```ts |
35 | | -import { reportingEndpoint } from 'reporting-api'; |
36 | 37 | import express from 'express'; |
| 38 | +import { reportingEndpoint, setupReportingHeaders } from 'reporting-api'; |
37 | 39 |
|
38 | 40 | const app = express(); |
39 | 41 |
|
40 | | -// The reporting endpoint. |
41 | | -// Use `use` to support CORS preflight request if you are receiving reports from another origin |
| 42 | +// 1. Mount the reporting endpoint |
42 | 43 | app.use('/reporting-endpoint', reportingEndpoint({ |
43 | | - allowedOrigins: '*', // Allow reports from all origins |
| 44 | + allowedOrigins: '*', |
44 | 45 | onReport(report) { |
45 | | - // Collect the reports and do what you want with them |
46 | | - console.log('Report received', { |
47 | | - isEnforced: report.body.type === 'enforce', |
48 | | - type: report.type, |
49 | | - body: report.body, |
50 | | - }); |
51 | | - } |
| 46 | + console.log(report.type, report.body); |
| 47 | + }, |
52 | 48 | })); |
53 | 49 |
|
54 | | -// Set the security headers |
55 | | -app.get('/*', (req, res, next) => { |
56 | | - // Set a CSP that disallows inline scripts. |
| 50 | +// 2. Set your policy headers, then let the middleware attach reporters |
| 51 | +app.use((req, res, next) => { |
57 | 52 | res.setHeader('Content-Security-Policy', "script-src 'self'"); |
58 | | - |
59 | | - // COOP policy that disallows link in new tab |
60 | 53 | res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); |
61 | | - |
62 | | - // COEP policy that disallows external resources that does not use CORS or CORP (Cross-Origin-Resource-Policy) |
63 | 54 | res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); |
64 | | - |
65 | | - // Setup headers alternative 1 |
66 | | - setupReportingHeaders('/reporting-endpoint')(req, res); |
67 | | - return next(); |
68 | | -}); |
69 | | -// Setup headers alternative 2 |
70 | | -app.get('/*', setupReportingHeaders('/reporting-endpoint', { |
71 | | - includeDefaultReporters: true, |
72 | | - enableNetworkErrorLogging: true, |
73 | | - version: '1', |
74 | | -})); |
75 | | -// |
76 | | -app.get('/test', (req, res) => { |
77 | | - res.writeHead(200, { |
78 | | - 'Content-Type': 'text/html; charset=utf8' |
79 | | - }); |
80 | | - |
81 | | - // The script will not run and instead generate a csp-violation report |
82 | | - // Clicking the link will trigger a COOP report |
83 | | - // Loading the image will trigger a COEP report |
84 | | - res.end(`Hello World! |
85 | | -<script>alert(1)</script> |
86 | | -<a href="https://google.com" target="_blank">Trigger COOP</> |
87 | | -<img src="https://lh3.googleusercontent.com/wAPeTvxh_EwOisF8kMR2L2eOrIOzjfA5AjE28W5asyfGeH85glwrO6zyqL71dCC26R63chADTO7DLOjnqRoXXOAB8t2f4C3QnU6o0BA"> |
88 | | -`); |
| 55 | + next(); |
89 | 56 | }); |
| 57 | +app.use(setupReportingHeaders('/reporting-endpoint')); |
90 | 58 |
|
91 | 59 | app.listen(8080); |
92 | 60 | ``` |
93 | 61 |
|
94 | 62 | > [!NOTE] |
95 | | -> The policy headers must be set before the reportingEndpointHeader middleware so the middleware is able to append the reporter to the policy headers. |
| 63 | +> Policy headers must be set **before** `setupReportingHeaders` runs so the middleware can append `report-to` and `report-uri` directives to them. |
| 64 | +
|
| 65 | +The resulting response headers will look like this: |
96 | 66 |
|
97 | | -### Response with a `Reporting-Endpoints` header created and reporter setup on the Policy headers |
98 | 67 | ``` |
99 | | -$ curl -I localhost:8080/test |
100 | | -Reporting-Endpoints: default=/test-endpoint |
101 | | -Content-Security-Policy: default-src 'self'; report-to default; report-uri /reporting-endpoint?src=report-uri |
102 | | -Cross-Origin-Opener-Policy: same-origin; report-to="default" |
103 | | -Cross-Origin-Embedder-Policy: require-corp; report-to="default" |
104 | | -
|
105 | | -Hello World! |
106 | | -<script>alert(1)</script> |
107 | | -<a href="https://google.com" target="_blank">Trigger COOP</> |
108 | | -<img src="https://lh3.googleusercontent.com/wAPeTvxh_EwOisF8kMR2L2eOrIOzjfA5AjE28W5asyfGeH85glwrO6zyqL71dCC26R63chADTO7DLOjnqRoXXOAB8t2f4C3QnU6o0BA"> |
| 68 | +Reporting-Endpoints: reporter="/reporting-endpoint" |
| 69 | +Content-Security-Policy: script-src 'self';report-uri /reporting-endpoint?disposition=enforce;report-to reporter |
| 70 | +Cross-Origin-Opener-Policy: same-origin;report-to="reporter" |
| 71 | +Cross-Origin-Embedder-Policy: require-corp;report-to="reporter" |
109 | 72 | ``` |
110 | 73 |
|
111 | | -> [!TIP] |
112 | | -> |
113 | | -> The Reporting API is also accessible in some browsers using the [ReportingObserver](https://developer.mozilla.org/en-US/docs/Web/API/ReportingObserver) |
114 | | -> ```js |
115 | | -> if (typeof ReportingObserver !== 'undefined') { |
116 | | -> const myObserver = new ReportingObserver(reportList => { |
117 | | -> reportList.forEach(report => { |
118 | | -> console.log(report.body); |
119 | | -> }); |
120 | | -> }); |
121 | | -> myObserver.observe(); |
122 | | -> } |
123 | | ->``` |
124 | | -
|
125 | | -## Configuration options |
126 | | -
|
127 | | -- [`reportingEndpoint`](./src/reporting-endpoint.ts) |
128 | | -- [`setupReportingHeaders`](./src/setup-headers.ts) |
| 74 | +## API |
129 | 75 |
|
130 | | -> [!NOTE] |
131 | | -> Set the `allowedOrigins` option on your reporting endpoint to allow cross origin reports. |
| 76 | +### `reportingEndpoint(config)` |
| 77 | + |
| 78 | +Returns Express middleware that accepts incoming reports. |
| 79 | + |
| 80 | +| Option | Type | Description | |
| 81 | +|--------|------|-------------| |
| 82 | +| `onReport` | `(report, req) => void` | Called for every valid report. | |
| 83 | +| `onValidationError` | `(error, body, req) => void` | Called when a report fails Zod validation. | |
| 84 | +| `allowedOrigins` | `string \| RegExp \| Array` | Enable CORS for cross-origin reports. Use `'*'` to allow any origin. | |
| 85 | +| `ignoreBrowserExtensions` | `boolean` | Drop CSP violations originating from browser extensions. | |
| 86 | +| `ignoredDeprecationIds` | `string[]` | Deprecation report IDs to ignore (e.g. `['AttributionReporting', 'Topics']`). | |
| 87 | +| `maxAge` | `number` | Maximum report age in **seconds**. Older buffered reports are dropped. | |
| 88 | +| `debug` | `boolean` | Enable `debug` logging for the `reporting-api:*` namespace. | |
| 89 | + |
| 90 | +### `setupReportingHeaders(url, config?)` |
| 91 | + |
| 92 | +Returns Express middleware that appends `report-to` / `report-uri` to every policy header already set on the response and adds the `Reporting-Endpoints` header. |
| 93 | + |
| 94 | +| Option | Type | Default | Description | |
| 95 | +|--------|------|---------|-------------| |
| 96 | +| `reportingGroup` | `string` | `"reporter"` | Reporting group name. | |
| 97 | +| `enableDefaultReporters` | `boolean` | `false` | Use the `default` group so you also receive Deprecation, Crash, and Intervention reports. | |
| 98 | +| `enableNetworkErrorLogging` | `boolean \| object` | `false` | Add `Report-To` + `NEL` headers (Reporting API v0, required for NEL). Accepts `{ success_fraction, failure_fraction, include_subdomains }`. | |
| 99 | +| `version` | `string \| number` | — | Appended as a `?version=` query param so you can correlate reports with policy revisions. | |
| 100 | + |
| 101 | +## Report schema |
| 102 | + |
| 103 | +Every report delivered to `onReport` is validated with Zod and has the shape: |
| 104 | + |
| 105 | +```ts |
| 106 | +{ |
| 107 | + type: 'csp-violation' | 'coop' | 'coep' | 'deprecation' | 'crash' |
| 108 | + | 'intervention' | 'network-error' | 'permissions-policy-violation' |
| 109 | + | 'potential-permissions-policy-violation'; |
| 110 | + body: { /* type-specific fields */ }; |
| 111 | + url: string; |
| 112 | + age: number; |
| 113 | + user_agent: string; |
| 114 | + report_format: 'report-uri' | 'report-to' | 'report-to-safari'; |
| 115 | + version?: string; |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +Full type definitions are exported as `Report` and the individual body types (`ContentSecurityPolicyReport`, `CrossOriginOpenerPolicyReport`, etc.). |
| 120 | + |
| 121 | +## Client-side observing |
| 122 | + |
| 123 | +Reports can also be observed in the browser via [ReportingObserver](https://developer.mozilla.org/en-US/docs/Web/API/ReportingObserver): |
| 124 | + |
| 125 | +```js |
| 126 | +if (typeof ReportingObserver !== 'undefined') { |
| 127 | + new ReportingObserver((reports) => { |
| 128 | + reports.forEach(r => console.log(r.body)); |
| 129 | + }).observe(); |
| 130 | +} |
| 131 | +``` |
132 | 132 |
|
133 | 133 | ## Resources |
134 | 134 |
|
135 | | -- [Permissions-Policy reporting](https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md) |
136 | | -- [Reporting API v0 and Reporting API v1 differences](https://chromium.googlesource.com/chromium/src/+/HEAD/net/reporting/README.md#supporting-both-v0-and-v1-reporting-in-the-same-codebase) |
| 135 | +- [Reporting API v1 spec (Reporting-Endpoints)](https://w3c.github.io/reporting/) |
| 136 | +- [Reporting API v0 spec (Report-To)](https://www.w3.org/TR/reporting/) |
137 | 137 | - [Migrating from v0 to v1](https://developer.chrome.com/blog/reporting-api-migration) |
138 | | -- [Reporting API v0 (Report-To)](https://www.w3.org/TR/reporting/) |
139 | | -- [Reporting API v1 (Reporting-Endpoints)](https://w3c.github.io/reporting/) |
| 138 | +- [v0 vs v1 differences (Chromium)](https://chromium.googlesource.com/chromium/src/+/HEAD/net/reporting/README.md#supporting-both-v0-and-v1-reporting-in-the-same-codebase) |
| 139 | +- [Permissions-Policy reporting](https://github.com/w3c/webappsec-permissions-policy/blob/main/reporting.md) |
140 | 140 |
|
141 | 141 | ### Notes |
142 | 142 |
|
143 | | -- `Permissions-Policy` reports to the `default` reporting group if `report-to` is not set. |
144 | | -- `report-to` group MUST be in double quotes (eg. `report-to="group"`) in COOP AND COEP headers to be used. |
145 | | -- [`Document-Policy`](https://wicg.github.io/document-policy/) and [`Document-Policy-Report-Only`](https://wicg.github.io/document-policy/) is doesn't look that supported or well documented, is it supersceded by Permissions-Policy? |
146 | | -- Safari sends reports in the format `body: { ... }` instead of an array of reports and it doesn't include an `age` |
| 143 | +- `Permissions-Policy` reports to the `default` group when `report-to` is not set. |
| 144 | +- COOP and COEP require `report-to` values wrapped in double quotes (e.g. `report-to="group"`). |
| 145 | +- Safari sends reports as `{ body: { ... } }` instead of an array and omits `age`. |
0 commit comments