@salesforce/pwa-kit-runtime 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/pwa-kit-runtime",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "The PWAKit Runtime",
|
|
5
5
|
"homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-runtime#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@loadable/component": "^5.15.3",
|
|
49
|
-
"@salesforce/pwa-kit-dev": "3.2.
|
|
49
|
+
"@salesforce/pwa-kit-dev": "3.2.1",
|
|
50
50
|
"@serverless/event-mocks": "^1.1.1",
|
|
51
51
|
"aws-lambda-mock-context": "^3.2.1",
|
|
52
52
|
"fs-extra": "^11.1.1",
|
|
53
|
-
"internal-lib-build": "3.2.
|
|
53
|
+
"internal-lib-build": "3.2.1",
|
|
54
54
|
"nock": "^13.3.0",
|
|
55
55
|
"nodemon": "^2.0.22",
|
|
56
56
|
"sinon": "^13.0.2",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"supertest": "^4.0.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@salesforce/pwa-kit-dev": "3.2.
|
|
61
|
+
"@salesforce/pwa-kit-dev": "3.2.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
64
64
|
"@salesforce/pwa-kit-dev": {
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"directory": "dist"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "2c8502478dc9f571f57042565d3d5938e46336a2"
|
|
76
76
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.once = exports.
|
|
6
|
+
exports.once = exports.RemoteServerFactory = exports.REMOTE_REQUIRED_ENV_VARS = void 0;
|
|
7
7
|
var _path = _interopRequireDefault(require("path"));
|
|
8
8
|
var _constants = require("./constants");
|
|
9
9
|
var _ssrServer = require("../../utils/ssr-server");
|
|
@@ -523,7 +523,6 @@ const RemoteServerFactory = {
|
|
|
523
523
|
|
|
524
524
|
// Apply the SSR middleware to any subsequent routes that we expect users
|
|
525
525
|
// to add in their projects, like in any regular Express app.
|
|
526
|
-
app.use(enforceSecurityHeaders); // Must be AFTER prepNonProxyRequest, as they both modify setHeader.
|
|
527
526
|
app.use(ssrMiddleware);
|
|
528
527
|
app.use(errorHandlerMiddleware);
|
|
529
528
|
applyPatches(options);
|
|
@@ -805,102 +804,6 @@ const RemoteServerFactory = {
|
|
|
805
804
|
}
|
|
806
805
|
};
|
|
807
806
|
|
|
808
|
-
/**
|
|
809
|
-
* Patches `res.setHeader` to ensure that the Content-Security-Policy header always includes the
|
|
810
|
-
* directives required for PWA Kit to work.
|
|
811
|
-
* @param {express.Request} req Express request object
|
|
812
|
-
* @param {express.Response} res Express response object
|
|
813
|
-
* @param {express.NextFunction} next Express next callback
|
|
814
|
-
*/
|
|
815
|
-
exports.RemoteServerFactory = RemoteServerFactory;
|
|
816
|
-
const enforceSecurityHeaders = (req, res, next) => {
|
|
817
|
-
/** CSP-compatible origin for Runtime Admin. */
|
|
818
|
-
// localhost doesn't include a protocol because different browsers behave differently :\
|
|
819
|
-
const runtimeAdmin = (0, _ssrServer.isRemote)() ? 'https://runtime.commercecloud.com' : 'localhost:*';
|
|
820
|
-
/**
|
|
821
|
-
* Map of directive names/values that are required for PWA Kit to work. Array values will be
|
|
822
|
-
* merged with user-provided values; boolean values will replace user-provided values.
|
|
823
|
-
* @type Object.<string, string[] | boolean>
|
|
824
|
-
*/
|
|
825
|
-
const directives = {
|
|
826
|
-
'connect-src': ["'self'", runtimeAdmin],
|
|
827
|
-
'frame-ancestors': [runtimeAdmin],
|
|
828
|
-
'img-src': ["'self'", 'data:'],
|
|
829
|
-
'script-src': ["'self'", "'unsafe-eval'", runtimeAdmin],
|
|
830
|
-
// Always upgrade insecure requests when deployed, never upgrade on local dev server
|
|
831
|
-
'upgrade-insecure-requests': (0, _ssrServer.isRemote)()
|
|
832
|
-
};
|
|
833
|
-
const setHeader = res.setHeader;
|
|
834
|
-
res.setHeader = (name, value) => {
|
|
835
|
-
let modifiedValue = value;
|
|
836
|
-
switch (name === null || name === void 0 ? void 0 : name.toLowerCase()) {
|
|
837
|
-
case _constants.CONTENT_SECURITY_POLICY:
|
|
838
|
-
{
|
|
839
|
-
// If multiple Content-Security-Policy headers are provided, then the most restrictive
|
|
840
|
-
// option is chosen for each directive. Therefore, we must modify *all* directives to
|
|
841
|
-
// ensure that our required directives will work as expected.
|
|
842
|
-
// Ref: https://w3c.github.io/webappsec-csp/#multiple-policies
|
|
843
|
-
modifiedValue = Array.isArray(value) ? value.map(item => modifyDirectives(item, directives)) : modifyDirectives(value, directives);
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
846
|
-
case _constants.STRICT_TRANSPORT_SECURITY:
|
|
847
|
-
{
|
|
848
|
-
// Block setting this header on local development server - it will break things!
|
|
849
|
-
if (!(0, _ssrServer.isRemote)()) return;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
|
-
default:
|
|
853
|
-
{
|
|
854
|
-
break;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return setHeader.call(res, name, modifiedValue);
|
|
858
|
-
};
|
|
859
|
-
// Provide an initial CSP (or patch the existing header)
|
|
860
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, res.getHeader(_constants.CONTENT_SECURITY_POLICY) ?? '');
|
|
861
|
-
// Provide an initial value for HSTS, if not already set - use default from `helmet`
|
|
862
|
-
if (!res.hasHeader(_constants.STRICT_TRANSPORT_SECURITY)) {
|
|
863
|
-
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=15552000; includeSubDomains');
|
|
864
|
-
}
|
|
865
|
-
next();
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Updates the given Content-Security-Policy header to include all directives required by PWA Kit.
|
|
870
|
-
* @param {string} original Original Content-Security-Policy header
|
|
871
|
-
* @returns {string} Modified Content-Security-Policy header
|
|
872
|
-
* @private
|
|
873
|
-
*/
|
|
874
|
-
exports.enforceSecurityHeaders = enforceSecurityHeaders;
|
|
875
|
-
const modifyDirectives = (original, required) => {
|
|
876
|
-
const directives = original.trim().split(';').reduce((acc, directive) => {
|
|
877
|
-
const text = directive.trim();
|
|
878
|
-
if (text) {
|
|
879
|
-
const [name, ...values] = text.split(/ +/);
|
|
880
|
-
acc[name] = values;
|
|
881
|
-
}
|
|
882
|
-
return acc;
|
|
883
|
-
}, {});
|
|
884
|
-
|
|
885
|
-
// Add missing required CSP directives
|
|
886
|
-
for (const [name, value] of Object.entries(required)) {
|
|
887
|
-
if (value === true) {
|
|
888
|
-
// Boolean directive (required) - overwrite original value
|
|
889
|
-
directives[name] = [];
|
|
890
|
-
} else if (value === false) {
|
|
891
|
-
// Boolean directive (disabled) - delete original value
|
|
892
|
-
delete directives[name];
|
|
893
|
-
} else {
|
|
894
|
-
// Regular string[] directive - merge values
|
|
895
|
-
// Wrapping with `[...new Set(array)]` removes duplicate entries
|
|
896
|
-
directives[name] = [...new Set([...(directives[name] ?? []), ...value])];
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Re-construct header string
|
|
901
|
-
return Object.entries(directives).map(([name, values]) => [name, ...values].join(' ')).join(';');
|
|
902
|
-
};
|
|
903
|
-
|
|
904
807
|
/**
|
|
905
808
|
* ExpressJS middleware that processes any non-proxy request passing
|
|
906
809
|
* through the Express app.
|
|
@@ -917,6 +820,7 @@ const modifyDirectives = (original, required) => {
|
|
|
917
820
|
*
|
|
918
821
|
* @private
|
|
919
822
|
*/
|
|
823
|
+
exports.RemoteServerFactory = RemoteServerFactory;
|
|
920
824
|
const prepNonProxyRequest = (req, res, next) => {
|
|
921
825
|
const options = req.app.options;
|
|
922
826
|
if (!options.allowCookies) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
var _buildRemoteServer = require("./build-remote-server");
|
|
4
|
-
var _constants = require("./constants");
|
|
5
4
|
/*
|
|
6
5
|
* Copyright (c) 2021, salesforce.com, inc.
|
|
7
6
|
* All rights reserved.
|
|
@@ -22,99 +21,4 @@ describe('the once function', () => {
|
|
|
22
21
|
expect(fn.mock.calls).toHaveLength(1);
|
|
23
22
|
expect(v1).toBe(v2); // The exact same instance
|
|
24
23
|
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('Content-Security-Policy enforcement', () => {
|
|
28
|
-
let res;
|
|
29
|
-
|
|
30
|
-
/** Sets the correct values for `isRemote()` to return true */
|
|
31
|
-
const mockProduction = () => {
|
|
32
|
-
process.env.AWS_LAMBDA_FUNCTION_NAME = 'testEnforceSecurityHeaders';
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Helper to make expected CSP more readable. Asserts that the actual CSP header contains each
|
|
36
|
-
* of the expected directives.
|
|
37
|
-
* @param {string[]} expected Array of expected CSP directives
|
|
38
|
-
*/
|
|
39
|
-
const expectDirectives = expected => {
|
|
40
|
-
const actual = res.getHeader(_constants.CONTENT_SECURITY_POLICY).split(';');
|
|
41
|
-
expect(actual).toEqual(expect.arrayContaining(expected));
|
|
42
|
-
};
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
const headers = {};
|
|
45
|
-
res = {
|
|
46
|
-
hasHeader: key => Object.hasOwn(headers, key),
|
|
47
|
-
getHeader: key => headers[key],
|
|
48
|
-
setHeader: (key, val) => headers[key] = val
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
// Revert state detected by `isRemote()`
|
|
52
|
-
afterEach(() => delete process.env.AWS_LAMBDA_FUNCTION_NAME);
|
|
53
|
-
test('adds required directives for development', () => {
|
|
54
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
55
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
56
|
-
expectDirectives(["connect-src 'self' localhost:*", 'frame-ancestors localhost:*', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' localhost:*"]);
|
|
57
|
-
});
|
|
58
|
-
test('adds required directives for production', () => {
|
|
59
|
-
mockProduction();
|
|
60
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
61
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
62
|
-
expectDirectives(["connect-src 'self' https://runtime.commercecloud.com", 'frame-ancestors https://runtime.commercecloud.com', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' https://runtime.commercecloud.com", 'upgrade-insecure-requests']);
|
|
63
|
-
});
|
|
64
|
-
test('merges with existing CSP directives', () => {
|
|
65
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
66
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, "connect-src test:* ; script-src 'unsafe-eval' test:*");
|
|
67
|
-
expectDirectives(["connect-src test:* 'self' localhost:*", "script-src 'unsafe-eval' test:* 'self' localhost:*"]);
|
|
68
|
-
});
|
|
69
|
-
test('allows other CSP directives', () => {
|
|
70
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
71
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'fake-directive test:*');
|
|
72
|
-
expectDirectives(['fake-directive test:*']);
|
|
73
|
-
});
|
|
74
|
-
test('enforces upgrade-insecure-requests disabled on development', () => {
|
|
75
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
76
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'upgrade-insecure-requests');
|
|
77
|
-
expect(res.getHeader(_constants.CONTENT_SECURITY_POLICY)).not.toContain('upgrade-insecure-requests');
|
|
78
|
-
});
|
|
79
|
-
test('enforces upgrade-insecure-requests enabled on production', () => {
|
|
80
|
-
mockProduction();
|
|
81
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
82
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'connect-src localhost:*');
|
|
83
|
-
expectDirectives(['upgrade-insecure-requests']);
|
|
84
|
-
});
|
|
85
|
-
test('adds directives even if setHeader is never called', () => {
|
|
86
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
87
|
-
expectDirectives(["img-src 'self' data:"]);
|
|
88
|
-
});
|
|
89
|
-
test('handles multiple CSP headers', () => {
|
|
90
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
91
|
-
res.setHeader(_constants.CONTENT_SECURITY_POLICY, ['connect-src first.header', 'script-src second.header']);
|
|
92
|
-
const headers = res.getHeader(_constants.CONTENT_SECURITY_POLICY);
|
|
93
|
-
expect(headers).toHaveLength(2);
|
|
94
|
-
expect(headers[0]).toContain('connect-src first.header');
|
|
95
|
-
expect(headers[1]).toContain('script-src second.header');
|
|
96
|
-
});
|
|
97
|
-
test('does not modify unrelated headers', () => {
|
|
98
|
-
const header = 'Contentious-Secret-Police';
|
|
99
|
-
const value = 'connect-src unmodified fake directive';
|
|
100
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
101
|
-
res.setHeader(header, value);
|
|
102
|
-
expect(res.getHeader(header)).toBe(value);
|
|
103
|
-
});
|
|
104
|
-
test('blocks Strict-Transport-Security header in development', () => {
|
|
105
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
106
|
-
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=12345');
|
|
107
|
-
expect(res.hasHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
test('allows Strict-Transport-Security header in production', () => {
|
|
110
|
-
mockProduction();
|
|
111
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
112
|
-
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=12345');
|
|
113
|
-
expect(res.getHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe('max-age=12345');
|
|
114
|
-
});
|
|
115
|
-
test('provides default value for Strict-Transport-Security header in production', () => {
|
|
116
|
-
mockProduction();
|
|
117
|
-
(0, _buildRemoteServer.enforceSecurityHeaders)({}, res, () => {});
|
|
118
|
-
expect(res.getHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe('max-age=15552000; includeSubDomains');
|
|
119
|
-
});
|
|
120
24
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _security = require("./security");
|
|
7
|
+
Object.keys(_security).forEach(function (key) {
|
|
8
|
+
if (key === "default" || key === "__esModule") return;
|
|
9
|
+
if (key in exports && exports[key] === _security[key]) return;
|
|
10
|
+
Object.defineProperty(exports, key, {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () {
|
|
13
|
+
return _security[key];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.defaultPwaKitSecurityHeaders = void 0;
|
|
7
|
+
var _constants = require("../../ssr/server/constants");
|
|
8
|
+
var _ssrServer = require("../ssr-server");
|
|
9
|
+
/*
|
|
10
|
+
* Copyright (c) 2023, Salesforce, Inc.
|
|
11
|
+
* All rights reserved.
|
|
12
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
13
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This express middleware sets the Content-Security-Policy and Strict-Transport-Security headers to
|
|
18
|
+
* default values that are required for PWA Kit to work. It also patches `res.setHeader` to allow
|
|
19
|
+
* additional CSP directives to be added without removing the required directives, and it prevents
|
|
20
|
+
* the Strict-Transport-Security header from being set on the local dev server.
|
|
21
|
+
* @param {express.Request} req Express request object
|
|
22
|
+
* @param {express.Response} res Express response object
|
|
23
|
+
* @param {express.NextFunction} next Express next callback
|
|
24
|
+
*/
|
|
25
|
+
const defaultPwaKitSecurityHeaders = (req, res, next) => {
|
|
26
|
+
/** CSP-compatible origin for Runtime Admin. */
|
|
27
|
+
// localhost doesn't include a protocol because different browsers behave differently :\
|
|
28
|
+
const runtimeAdmin = (0, _ssrServer.isRemote)() ? 'https://runtime.commercecloud.com' : 'localhost:*';
|
|
29
|
+
/**
|
|
30
|
+
* Map of directive names/values that are required for PWA Kit to work. Array values will be
|
|
31
|
+
* merged with user-provided values; boolean values will replace user-provided values.
|
|
32
|
+
* @type Object.<string, string[] | boolean>
|
|
33
|
+
*/
|
|
34
|
+
const directives = {
|
|
35
|
+
'connect-src': ["'self'", runtimeAdmin],
|
|
36
|
+
'frame-ancestors': [runtimeAdmin],
|
|
37
|
+
'img-src': ["'self'", 'data:'],
|
|
38
|
+
'script-src': ["'self'", "'unsafe-eval'", runtimeAdmin],
|
|
39
|
+
// Always upgrade insecure requests when deployed, never upgrade on local dev server
|
|
40
|
+
'upgrade-insecure-requests': (0, _ssrServer.isRemote)()
|
|
41
|
+
};
|
|
42
|
+
const setHeader = res.setHeader;
|
|
43
|
+
res.setHeader = (name, value) => {
|
|
44
|
+
let modifiedValue = value;
|
|
45
|
+
switch (name === null || name === void 0 ? void 0 : name.toLowerCase()) {
|
|
46
|
+
case _constants.CONTENT_SECURITY_POLICY:
|
|
47
|
+
{
|
|
48
|
+
// If multiple Content-Security-Policy headers are provided, then the most restrictive
|
|
49
|
+
// option is chosen for each directive. Therefore, we must modify *all* directives to
|
|
50
|
+
// ensure that our required directives will work as expected.
|
|
51
|
+
// Ref: https://w3c.github.io/webappsec-csp/#multiple-policies
|
|
52
|
+
modifiedValue = Array.isArray(value) ? value.map(item => modifyDirectives(item, directives)) : modifyDirectives(value, directives);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case _constants.STRICT_TRANSPORT_SECURITY:
|
|
56
|
+
{
|
|
57
|
+
// Block setting this header on local development server - it will break things!
|
|
58
|
+
if (!(0, _ssrServer.isRemote)()) return;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
{
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return setHeader.call(res, name, modifiedValue);
|
|
67
|
+
};
|
|
68
|
+
// Provide an initial CSP (or patch the existing header)
|
|
69
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, res.getHeader(_constants.CONTENT_SECURITY_POLICY) ?? '');
|
|
70
|
+
// Provide an initial value for HSTS, if not already set - use default from `helmet`
|
|
71
|
+
if (!res.hasHeader(_constants.STRICT_TRANSPORT_SECURITY)) {
|
|
72
|
+
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=15552000; includeSubDomains');
|
|
73
|
+
}
|
|
74
|
+
next();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Updates the given Content-Security-Policy header to include all directives required by PWA Kit.
|
|
79
|
+
* @param {string} original Original Content-Security-Policy header
|
|
80
|
+
* @returns {string} Modified Content-Security-Policy header
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
exports.defaultPwaKitSecurityHeaders = defaultPwaKitSecurityHeaders;
|
|
84
|
+
const modifyDirectives = (original, required) => {
|
|
85
|
+
const directives = original.trim().split(';').reduce((acc, directive) => {
|
|
86
|
+
const text = directive.trim();
|
|
87
|
+
if (text) {
|
|
88
|
+
const [name, ...values] = text.split(/ +/);
|
|
89
|
+
acc[name] = values;
|
|
90
|
+
}
|
|
91
|
+
return acc;
|
|
92
|
+
}, {});
|
|
93
|
+
|
|
94
|
+
// Add missing required CSP directives
|
|
95
|
+
for (const [name, value] of Object.entries(required)) {
|
|
96
|
+
if (value === true) {
|
|
97
|
+
// Boolean directive (required) - overwrite original value
|
|
98
|
+
directives[name] = [];
|
|
99
|
+
} else if (value === false) {
|
|
100
|
+
// Boolean directive (disabled) - delete original value
|
|
101
|
+
delete directives[name];
|
|
102
|
+
} else {
|
|
103
|
+
// Regular string[] directive - merge values
|
|
104
|
+
// Wrapping with `[...new Set(array)]` removes duplicate entries
|
|
105
|
+
directives[name] = [...new Set([...(directives[name] ?? []), ...value])];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Re-construct header string
|
|
110
|
+
return Object.entries(directives).map(([name, values]) => [name, ...values].join(' ')).join(';');
|
|
111
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _constants = require("../../ssr/server/constants");
|
|
4
|
+
var _security = require("./security");
|
|
5
|
+
/*
|
|
6
|
+
* Copyright (c) 2023, Salesforce, Inc.
|
|
7
|
+
* All rights reserved.
|
|
8
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
9
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
describe('Content-Security-Policy enforcement', () => {
|
|
13
|
+
let res;
|
|
14
|
+
|
|
15
|
+
/** Sets the correct values for `isRemote()` to return true */
|
|
16
|
+
const mockProduction = () => {
|
|
17
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'testEnforceSecurityHeaders';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Helper to make expected CSP more readable. Asserts that the actual CSP header contains each
|
|
21
|
+
* of the expected directives.
|
|
22
|
+
* @param {string[]} expected Array of expected CSP directives
|
|
23
|
+
*/
|
|
24
|
+
const expectDirectives = expected => {
|
|
25
|
+
const actual = res.getHeader(_constants.CONTENT_SECURITY_POLICY).split(';');
|
|
26
|
+
expect(actual).toEqual(expect.arrayContaining(expected));
|
|
27
|
+
};
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
const headers = {};
|
|
30
|
+
res = {
|
|
31
|
+
hasHeader: key => Object.hasOwn(headers, key),
|
|
32
|
+
getHeader: key => headers[key],
|
|
33
|
+
setHeader: (key, val) => headers[key] = val
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
// Revert state detected by `isRemote()`
|
|
37
|
+
afterEach(() => delete process.env.AWS_LAMBDA_FUNCTION_NAME);
|
|
38
|
+
test('adds required directives for development', () => {
|
|
39
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
40
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
41
|
+
expectDirectives(["connect-src 'self' localhost:*", 'frame-ancestors localhost:*', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' localhost:*"]);
|
|
42
|
+
});
|
|
43
|
+
test('adds required directives for production', () => {
|
|
44
|
+
mockProduction();
|
|
45
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
46
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
47
|
+
expectDirectives(["connect-src 'self' https://runtime.commercecloud.com", 'frame-ancestors https://runtime.commercecloud.com', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' https://runtime.commercecloud.com", 'upgrade-insecure-requests']);
|
|
48
|
+
});
|
|
49
|
+
test('merges with existing CSP directives', () => {
|
|
50
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
51
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, "connect-src test:* ; script-src 'unsafe-eval' test:*");
|
|
52
|
+
expectDirectives(["connect-src test:* 'self' localhost:*", "script-src 'unsafe-eval' test:* 'self' localhost:*"]);
|
|
53
|
+
});
|
|
54
|
+
test('allows other CSP directives', () => {
|
|
55
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
56
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'fake-directive test:*');
|
|
57
|
+
expectDirectives(['fake-directive test:*']);
|
|
58
|
+
});
|
|
59
|
+
test('enforces upgrade-insecure-requests disabled on development', () => {
|
|
60
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
61
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'upgrade-insecure-requests');
|
|
62
|
+
expect(res.getHeader(_constants.CONTENT_SECURITY_POLICY)).not.toContain('upgrade-insecure-requests');
|
|
63
|
+
});
|
|
64
|
+
test('enforces upgrade-insecure-requests enabled on production', () => {
|
|
65
|
+
mockProduction();
|
|
66
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
67
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, 'connect-src localhost:*');
|
|
68
|
+
expectDirectives(['upgrade-insecure-requests']);
|
|
69
|
+
});
|
|
70
|
+
test('adds directives even if setHeader is never called', () => {
|
|
71
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
72
|
+
expectDirectives(["img-src 'self' data:"]);
|
|
73
|
+
});
|
|
74
|
+
test('handles multiple CSP headers', () => {
|
|
75
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
76
|
+
res.setHeader(_constants.CONTENT_SECURITY_POLICY, ['connect-src first.header', 'script-src second.header']);
|
|
77
|
+
const headers = res.getHeader(_constants.CONTENT_SECURITY_POLICY);
|
|
78
|
+
expect(headers).toHaveLength(2);
|
|
79
|
+
expect(headers[0]).toContain('connect-src first.header');
|
|
80
|
+
expect(headers[1]).toContain('script-src second.header');
|
|
81
|
+
});
|
|
82
|
+
test('does not modify unrelated headers', () => {
|
|
83
|
+
const header = 'Contentious-Secret-Police';
|
|
84
|
+
const value = 'connect-src unmodified fake directive';
|
|
85
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
86
|
+
res.setHeader(header, value);
|
|
87
|
+
expect(res.getHeader(header)).toBe(value);
|
|
88
|
+
});
|
|
89
|
+
test('blocks Strict-Transport-Security header in development', () => {
|
|
90
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
91
|
+
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=12345');
|
|
92
|
+
expect(res.hasHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
test('allows Strict-Transport-Security header in production', () => {
|
|
95
|
+
mockProduction();
|
|
96
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
97
|
+
res.setHeader(_constants.STRICT_TRANSPORT_SECURITY, 'max-age=12345');
|
|
98
|
+
expect(res.getHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe('max-age=12345');
|
|
99
|
+
});
|
|
100
|
+
test('provides default value for Strict-Transport-Security header in production', () => {
|
|
101
|
+
mockProduction();
|
|
102
|
+
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
103
|
+
expect(res.getHeader(_constants.STRICT_TRANSPORT_SECURITY)).toBe('max-age=15552000; includeSubDomains');
|
|
104
|
+
});
|
|
105
|
+
});
|