Skip to content
This repository was archived by the owner on Oct 29, 2025. It is now read-only.

Commit 93c2aa2

Browse files
authored
Merge pull request #183 from PerimeterX/dev
Dev to master
2 parents baee408 + 16bfaa5 commit 93c2aa2

File tree

9 files changed

+105
-6
lines changed

9 files changed

+105
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [3.0.2] - 2021-11-14
9+
10+
### Added
11+
12+
- Nonce support in CSP header
13+
814
## [3.0.1] - 2021-10-25
915

1016
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers
77
=============================================================
88

9-
> Latest stable version: [v3.0.1](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.0.2](https://www.npmjs.com/package/perimeterx-node-core)
1010
1111
This is a shared base implementation for PerimeterX Express enforcer and future NodeJS enforcers. For a fully functioning implementation example, see the [Node-Express enforcer](https://github.com/PerimeterX/perimeterx-node-express/) implementation.
1212

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ module.exports = {
2727
PxEnforcer: require('./lib/pxenforcer'),
2828
PxClient: require('./lib/pxclient'),
2929
PxCdEnforcer: require('./lib/pxcdenforcer'),
30+
addNonce: require('./lib/nonce')
3031
};

lib/nonce.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const { CSP_HEADER, CSPRO_HEADER } = require('./utils/constants');
2+
const SCRIPT_SRC_STRING = 'script-src ';
3+
4+
function addNonce(response, nonce) {
5+
if (validateNonce(nonce)) {
6+
addNonceToHeader(response, CSP_HEADER, nonce);
7+
addNonceToHeader(response, CSPRO_HEADER, nonce);
8+
} else {
9+
console.error('nonce value is not valid, will not be added to CSP header');
10+
}
11+
}
12+
13+
function addNonceToHeader(response, headerName, nonce) {
14+
let headerValue = response.getHeader(headerName);
15+
if (headerValue) {
16+
const matches = headerValue.match(/script-src ([^ ;]+)/);
17+
if (matches && matches.length > 1) {
18+
const updatedScriptSrc = `'nonce-${nonce}' ` + matches[1];
19+
const index = matches.index + matches[0].length;
20+
headerValue = headerValue.slice(0, matches.index + SCRIPT_SRC_STRING.length) + updatedScriptSrc + headerValue.slice(index);
21+
response.setHeader(headerName, headerValue);
22+
} else {
23+
//add script-src
24+
const index = headerValue.indexOf(';') + 1;
25+
headerValue = headerValue.slice(0, index) + ` ${SCRIPT_SRC_STRING}'nonce-${nonce}';` + headerValue.slice(index);
26+
response.setHeader(headerName, headerValue);
27+
}
28+
}
29+
}
30+
31+
function validateNonce(nonce) {
32+
const re = /^[A-Za-z0-9=]+$/;
33+
return re.test(nonce);
34+
}
35+
36+
module.exports = addNonce;

lib/pxcdenforcer.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const CSP_DATA = {
1010
REPORT_URI_STRING_LENGTH: 11
1111
};
1212
const CD_COOCKIE = '__pxvid';
13+
const { CSP_HEADER, CSPRO_HEADER } = require('./utils/constants');
1314

1415
function CdEnforce (cspData, req, res) {
1516
const cdVid = getCdCookie(req);
@@ -24,16 +25,16 @@ function handleCSP(response, cspData, vid) {
2425
const sessionId = uuidv4();
2526
if (csp) {
2627
csp = updateCspReportUri(csp, sessionId, vid);
27-
response.setHeader('Content-Security-Policy', csp);
28+
response.setHeader(CSP_HEADER, csp);
2829
}
2930

3031
let cspReportOnly = getCSPROHeader(cspData, CSP_DATA.CSPRO_EXPOSURE, CSP_DATA.CSPRO, rand, sessionId, vid);
3132
if (cspReportOnly) {
32-
response.setHeader('Content-Security-Policy-Report-Only', cspReportOnly);
33+
response.setHeader(CSPRO_HEADER, cspReportOnly);
3334
} else {
3435
cspReportOnly = getCSPROHeader(cspData, CSP_DATA.CSPRO_2_EXPOSURE, CSP_DATA.CSPRO_2, rand, sessionId, vid);
3536
if (cspReportOnly) {
36-
response.setHeader('Content-Security-Policy-Report-Only', cspReportOnly);
37+
response.setHeader(CSPRO_HEADER, cspReportOnly);
3738
}
3839
}
3940
} catch (e) {

lib/utils/constants.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ const MILLISECONDS_IN_YEAR = MILLISECONDS_IN_SECOND * SECONDS_IN_MINUTE * MINUTE
77

88
const DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME = 'px-compromised-credentials';
99

10+
const CSP_HEADER = 'Content-Security-Policy';
11+
const CSPRO_HEADER = 'Content-Security-Policy-Report-Only';
12+
1013
module.exports = {
1114
MILLISECONDS_IN_SECOND,
1215
SECONDS_IN_MINUTE,
1316
MINUTES_IN_HOUR,
1417
HOURS_IN_DAY,
1518
DAYS_IN_YEAR,
1619
MILLISECONDS_IN_YEAR,
17-
DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME
20+
DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME,
21+
CSP_HEADER,
22+
CSPRO_HEADER
1823
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "perimeterx-node-core",
3-
"version": "3.0.1",
3+
"version": "3.0.2",
44
"description": "PerimeterX NodeJS shared core for various applications to monitor and block traffic according to PerimeterX risk score",
55
"main": "index.js",
66
"scripts": {

test/mocks/response.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class Response {
2+
constructor(headers) {
3+
this.headers = headers;
4+
}
5+
6+
getHeader(headerName) {
7+
return this.headers[headerName];
8+
}
9+
10+
setHeader(headerName, headerValue) {
11+
this.headers[headerName] = headerValue;
12+
}
13+
}
14+
15+
module.exports = Response;

test/pxenforcer.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const PxClient = rewire('../lib/pxclient');
99
const PxEnforcer = require('../lib/pxenforcer');
1010
const proxyquire = require('proxyquire');
1111
const { ModuleMode } = require('../lib/enums/ModuleMode');
12+
const Response = require('./mocks/response');
13+
const addNonce = require('../lib/nonce');
14+
const { CSP_HEADER, CSPRO_HEADER } = require('../lib/utils/constants');
1215

1316
describe('PX Enforcer - pxenforcer.js', () => {
1417
let params, enforcer, req, stub, pxClient, pxLoggerSpy, logger;
@@ -804,4 +807,36 @@ describe('PX Enforcer - pxenforcer.js', () => {
804807
done();
805808
});
806809
});
810+
811+
it('Should add Nonce to CSP header (script-src directive exists)', (done) => {
812+
const nonce = 'ImN0nc3Value';
813+
const headerWithoutNonce = 'connect-src \'self\' *.bazaarvoice.com *.google.com *.googleapis.com *.perimeterx.net *.px-cdn.net *.px-client.net; script-src \'self\' \'unsafe-eval\' \'unsafe-inline\' *.bazaarvoice.com *.forter.com *.google-analytics.com report-uri https://csp.px-cloud.net/report?report=1&id=8a3a7c5242c0e7646bd7d86284f408f6&app_id=PXFF0j69T5&p=d767ae06-b964-4b42-96a2-6d4089aab525';
814+
const headerWithNonce = 'connect-src \'self\' *.bazaarvoice.com *.google.com *.googleapis.com *.perimeterx.net *.px-cdn.net *.px-client.net; script-src \'nonce-ImN0nc3Value\' \'self\' \'unsafe-eval\' \'unsafe-inline\' *.bazaarvoice.com *.forter.com *.google-analytics.com report-uri https://csp.px-cloud.net/report?report=1&id=8a3a7c5242c0e7646bd7d86284f408f6&app_id=PXFF0j69T5&p=d767ae06-b964-4b42-96a2-6d4089aab525';
815+
nonceTestUtil(headerWithoutNonce, headerWithNonce);
816+
done();
817+
});
818+
819+
it('Should add Nonce to CSP header (script-src directive does not exists)', (done) => {
820+
const headerWithoutNonce = 'connect-src \'self\' https://collector-px8u0i7rwc.px-cdn.net https://collector-px8u0i7rwc.px-cloud.net; report-uri https://csp.px-cloud.net/report?report=1&id=4bd43ac663997dde7c6a84abd14fdd7a&app_id=PX8U0i7rwC&p=70bb7c94-4807-4090-bea4-ffd1f7645126';
821+
const headerWithNonce = 'connect-src \'self\' https://collector-px8u0i7rwc.px-cdn.net https://collector-px8u0i7rwc.px-cloud.net; script-src \'nonce-ImN0nc3Value\'; report-uri https://csp.px-cloud.net/report?report=1&id=4bd43ac663997dde7c6a84abd14fdd7a&app_id=PX8U0i7rwC&p=70bb7c94-4807-4090-bea4-ffd1f7645126';
822+
nonceTestUtil(headerWithoutNonce, headerWithNonce);
823+
done();
824+
});
825+
826+
it('Should add Nonce to CSP header, CSP header empty', (done) => {
827+
const headerWithoutNonce = ';';
828+
const headerWithNonce = '; script-src \'nonce-ImN0nc3Value\';';
829+
nonceTestUtil(headerWithoutNonce, headerWithNonce);
830+
done();
831+
});
807832
});
833+
834+
const nonceTestUtil = (headerWithoutNonce, headerWithNonce) => {
835+
const nonce = 'ImN0nc3Value';
836+
const headers = { [CSP_HEADER]: headerWithoutNonce, [CSPRO_HEADER]: headerWithoutNonce };
837+
const response = new Response(headers);
838+
addNonce(response, nonce);
839+
840+
(response.getHeader(CSP_HEADER) === headerWithNonce).should.equal(true);
841+
(response.getHeader(CSPRO_HEADER) === headerWithNonce).should.equal(true);
842+
};

0 commit comments

Comments
 (0)