Skip to content

Commit 873a5ec

Browse files
authored
Merge pull request #11 from pavkir/master
Support reporting-endpoints headers
2 parents 475e293 + 638c8bc commit 873a5ec

File tree

3 files changed

+108
-13
lines changed

3 files changed

+108
-13
lines changed

packages/express-csp-header/README.md

+7-12
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,25 @@ app.use(expressCspHeader({
8080
```
8181

8282
## CSP violation report
83-
For more information read [csp-header documentation](https://github.com/frux/csp/tree/master/packages/csp-header#csp-violation-report). `express-csp-header` helps you manage both `Content-Security-Policy` and `Report-To` headers.
83+
For more information read [csp-header documentation](https://github.com/frux/csp/tree/master/packages/csp-header#csp-violation-report). `express-csp-header` helps you manage both `Content-Security-Policy` and `Reporting-Endpoints` headers. [Report-to headers is no longer recommended to use](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Report-To)
8484

8585
```js
8686
const { expressCspHeader, INLINE, NONE, SELF } = require('express-csp-header');
8787

8888
app.use(expressCspHeader({
8989
directives: {
9090
'default-src': [SELF],
91-
'report-to': 'my-report-group'
91+
'report-to': 'csp-default'
9292
},
9393
reportUri: 'https://cspreport.com/send',
94-
reportTo: [
95-
{
96-
group: 'my-report-group',
97-
max_age: 30 * 60,
98-
endpoints: [{ url: 'https://cspreport.com/send'}],
99-
include_subdomains: true
100-
}
101-
]
94+
reportingEndpoints: [
95+
{'csp-default': 'https://cspreport.com/send'}
96+
]
10297
}));
10398

10499
/* express will send two headers
105-
1. Content-Security-Policy: default-src 'self'; report-to my-report-group; report-uri https://cspreport.com/send;
106-
2. Report-To: {"group":"my-report-group","max_age":1800,"endpoints":[{"url":"https://cspreport.com/send"}],"include_subdomains":true}
100+
1. Content-Security-Policy: default-src 'self'; report-to csp-default; report-uri https://cspreport.com/send;
101+
2. Reporting-Endpoints: csp-default="https://cspreport.com/send"
107102
*/
108103
```
109104

packages/express-csp-header/src/index.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export * from './constants';
1111
type ReportUriFunction = (req: Request, res: Response) => string;
1212
type ReportToFunction = (req: Request, res: Response) => ReportTo[];
1313

14+
export interface ReportingEndpoint {
15+
name: string;
16+
uri: string;
17+
}
18+
1419
export interface ReportTo {
1520
group: string;
1621
max_age: number;
@@ -27,10 +32,11 @@ export interface ExpressCSPParams extends Omit<CSPHeaderParams, 'reportUri'> {
2732
reportOnly?: boolean,
2833
reportTo?: ReportTo[] | ReportToFunction,
2934
reportUri?: string | ReportUriFunction,
35+
reportingEndpoints?: ReportingEndpoint[] | ((req: Request, res: Response) => ReportingEndpoint[]),
3036
}
3137

3238
export function expressCspHeader(params?: ExpressCSPParams): RequestHandler {
33-
return function (req, res, next) {
39+
return function (req: Request, res: Response, next) {
3440
if (!params) {
3541
next();
3642
return;
@@ -51,6 +57,17 @@ export function expressCspHeader(params?: ExpressCSPParams): RequestHandler {
5157
res.set(REPORT_TO_HEADER, reportTo.map(group => JSON.stringify(group)).join(','));
5258
}
5359

60+
const reportingEndpoints = typeof params.reportingEndpoints === 'function' ?
61+
params.reportingEndpoints(req, res) :
62+
params.reportingEndpoints;
63+
64+
if (reportingEndpoints) {
65+
const endpointsString = reportingEndpoints
66+
.map((endpoint) => `${endpoint.name}="${encodeURI(endpoint.uri)}"`)
67+
.join(', ');
68+
res.set(REPORTING_ENDPOINTS_HEADER, endpointsString);
69+
}
70+
5471
next();
5572
};
5673
}
@@ -119,3 +136,4 @@ function parseDomain(hostname: string, domainOptions?: ParseOptions): string | n
119136
const CSP_HEADER = 'Content-Security-Policy';
120137
const CSP_REPORT_ONLY_HEADER = 'Content-Security-Policy-Report-Only';
121138
const REPORT_TO_HEADER = 'Report-To';
139+
const REPORTING_ENDPOINTS_HEADER = 'Reporting-Endpoints';

packages/express-csp-header/tests/index.test.ts

+82
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,85 @@ describe('Report-To', () => {
223223
);
224224
});
225225
});
226+
227+
describe('Reporting-Endpoints', () => {
228+
test('should set Reporting-Endpoints header with static endpoints', () => {
229+
const { res } = execMiddleware({
230+
directives: {
231+
'default-src': [SELF],
232+
'report-to': 'csp-endpoint'
233+
},
234+
reportingEndpoints: [
235+
{ name: 'endpoint-1', uri: 'https://reports.example/main' },
236+
{ name: 'csp-endpoint', uri: 'https://reports.example/csp' }
237+
]
238+
});
239+
expect(res.headers['Content-Security-Policy']).toEqual(
240+
"default-src 'self'; report-to csp-endpoint;"
241+
);
242+
expect(res.headers['Reporting-Endpoints']).toEqual(
243+
'endpoint-1="https://reports.example/main", csp-endpoint="https://reports.example/csp"'
244+
);
245+
});
246+
247+
test('should support specifying reporting endpoints as a function', () => {
248+
const { res } = execMiddleware({
249+
directives: {
250+
'default-src': [SELF],
251+
},
252+
reportingEndpoints: (req) => [
253+
{ name: 'main', uri: `https://reports.example/${req.hostname}/main` },
254+
{ name: 'csp', uri: `https://reports.example/${req.hostname}/csp` }
255+
]
256+
}, { hostname: 'test.com' });
257+
258+
expect(res.headers['Content-Security-Policy']).toEqual(
259+
"default-src 'self';"
260+
);
261+
expect(res.headers['Reporting-Endpoints']).toEqual(
262+
'main="https://reports.example/test.com/main", csp="https://reports.example/test.com/csp"'
263+
);
264+
});
265+
test('should support specifying reporting endpoints as a function with special characters', () => {
266+
const { res } = execMiddleware({
267+
directives: {
268+
'default-src': [SELF],
269+
},
270+
reportingEndpoints: (req) => [
271+
{ name: 'csp-reports', uri: `https://reports.example/${req.headers.login}/csp` }
272+
]
273+
}, { headers: {'login':'Abobus"\' Test' }});
274+
275+
expect(res.headers['Content-Security-Policy']).toEqual(
276+
"default-src 'self';"
277+
);
278+
expect(res.headers['Reporting-Endpoints']).toEqual(
279+
'csp-reports="https://reports.example/Abobus%22\'%20Test/csp"'
280+
);
281+
});
282+
283+
test('should work together with Report-To header', () => {
284+
const { res } = execMiddleware({
285+
directives: {
286+
'default-src': [SELF],
287+
'report-to': 'my-report-group'
288+
},
289+
reportTo: [{
290+
group: 'my-report-group',
291+
max_age: 1800,
292+
endpoints: [{ url: 'https://reports.example/report-to' }],
293+
include_subdomains: true
294+
}],
295+
reportingEndpoints: [
296+
{ name: 'endpoint-1', uri: 'https://reports.example/endpoints' }
297+
]
298+
});
299+
300+
expect(res.headers['Report-To']).toEqual(
301+
'{"group":"my-report-group","max_age":1800,"endpoints":[{"url":"https://reports.example/report-to"}],"include_subdomains":true}'
302+
);
303+
expect(res.headers['Reporting-Endpoints']).toEqual(
304+
'endpoint-1="https://reports.example/endpoints"'
305+
);
306+
});
307+
});

0 commit comments

Comments
 (0)