@contrast/protect 1.0.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/LICENSE +12 -0
- package/README.md +9 -0
- package/lib/cli-rewriter.js +20 -0
- package/lib/error-handlers/constants.js +5 -0
- package/lib/error-handlers/index.js +13 -0
- package/lib/error-handlers/install/fastify3.js +88 -0
- package/lib/error-handlers/install/fastify3.test.js +142 -0
- package/lib/esm-loader.mjs +2 -0
- package/lib/esm-loader.test.mjs +11 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +89 -0
- package/lib/index.test.js +32 -0
- package/lib/input-analysis/handlers.js +462 -0
- package/lib/input-analysis/handlers.test.js +898 -0
- package/lib/input-analysis/index.js +16 -0
- package/lib/input-analysis/index.test.js +28 -0
- package/lib/input-analysis/install/fastify3.js +79 -0
- package/lib/input-analysis/install/fastify3.test.js +71 -0
- package/lib/input-analysis/install/http.js +185 -0
- package/lib/input-analysis/install/http.test.js +315 -0
- package/lib/input-tracing/constants.js +5 -0
- package/lib/input-tracing/handlers/index.js +117 -0
- package/lib/input-tracing/handlers/index.test.js +395 -0
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
- package/lib/input-tracing/index.js +32 -0
- package/lib/input-tracing/install/README.md +1 -0
- package/lib/input-tracing/install/child-process.js +45 -0
- package/lib/input-tracing/install/child-process.test.js +112 -0
- package/lib/input-tracing/install/fs.js +107 -0
- package/lib/input-tracing/install/fs.test.js +118 -0
- package/lib/input-tracing/install/mysql.js +57 -0
- package/lib/input-tracing/install/mysql.test.js +108 -0
- package/lib/input-tracing/install/postgres.js +61 -0
- package/lib/input-tracing/install/postgres.test.js +125 -0
- package/lib/input-tracing/install/sequelize.js +51 -0
- package/lib/input-tracing/install/sequelize.test.js +79 -0
- package/lib/input-tracing/install/sqlite3.js +45 -0
- package/lib/input-tracing/install/sqlite3.test.js +88 -0
- package/lib/make-response-blocker.js +35 -0
- package/lib/make-response-blocker.test.js +88 -0
- package/lib/make-source-context.js +130 -0
- package/lib/make-source-context.test.js +298 -0
- package/lib/security-exception.js +12 -0
- package/lib/throw-security-exception.js +30 -0
- package/lib/throw-security-exception.test.js +50 -0
- package/lib/utils.js +88 -0
- package/lib/utils.test.js +40 -0
- package/package.json +32 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
|
|
6
|
+
const mocks = require('../../test/mocks');
|
|
7
|
+
|
|
8
|
+
// rules in config
|
|
9
|
+
const rulesCfg = [
|
|
10
|
+
{ id: 'bot-blocker', where: 'agent-lib-input' },
|
|
11
|
+
{ id: 'cmd-injection', where: 'agent-lib-input' },
|
|
12
|
+
{ id: 'cmd-injection-command-backdoors', where: 'agent' },
|
|
13
|
+
{ id: 'cmd-injection-semantic-chained-commands', where: 'agent' },
|
|
14
|
+
{ id: 'cmd-injection-semantic-dangerous-paths', where: 'agent' },
|
|
15
|
+
{ id: 'ip-denylist', where: 'agent' },
|
|
16
|
+
{ id: 'method-tampering', where: 'agent-lib-input' },
|
|
17
|
+
{ id: 'nosql-injection-mongo', where: 'agent-lib-input' },
|
|
18
|
+
{ id: 'path-traversal', where: 'agent-lib-input' },
|
|
19
|
+
{ id: 'reflected-xss', where: 'agent-lib-input' },
|
|
20
|
+
{ id: 'sql-injection', where: 'agent-lib-input' },
|
|
21
|
+
{ id: 'ssjs-injection', where: 'agent-lib-input' },
|
|
22
|
+
{ id: 'virtual-patch', where: 'agent' },
|
|
23
|
+
{ id: 'untrusted-deserialization', where: 'agent' },
|
|
24
|
+
{ id: 'unsafe-file-upload', where: 'agent-lib-input' },
|
|
25
|
+
{ id: 'xxe', where: 'agent' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const rules = rulesCfg.map((r) => r.id);
|
|
29
|
+
const agentLibRules = rulesCfg.filter((r) => r.where === 'agent-lib-input').map((r) => r.id);
|
|
30
|
+
const agentRules = rulesCfg.filter((r) => r.where !== 'agent-lib-input').map((r) => r.id);
|
|
31
|
+
|
|
32
|
+
/* eslint-disable newline-per-chained-call */
|
|
33
|
+
|
|
34
|
+
describe('protect make-source-context', function() {
|
|
35
|
+
let core;
|
|
36
|
+
|
|
37
|
+
beforeEach(function() {
|
|
38
|
+
core = mocks.core();
|
|
39
|
+
core.logger = mocks.logger();
|
|
40
|
+
core.config = mocks.config();
|
|
41
|
+
core.scopes = mocks.scopes();
|
|
42
|
+
core.config.protect.rules = makeProtectRulesConfig(rules);
|
|
43
|
+
core.scopes = mocks.scopes();
|
|
44
|
+
require('../../protect')(core);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('mock core.config.protect.rules are initialized correctly', function() {
|
|
48
|
+
// shorthand
|
|
49
|
+
const protectRules = core.config.protect.rules;
|
|
50
|
+
for (const ruleId of rules) {
|
|
51
|
+
expect(protectRules).property(ruleId).eql({ mode: 'monitor' });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('core.protect.rules is initialized correctly from mock config', function() {
|
|
56
|
+
const { protect } = core;
|
|
57
|
+
|
|
58
|
+
expect(protect).property('agentLib').an('object');
|
|
59
|
+
expect(protect.agentLib).property('RuleType').eql({
|
|
60
|
+
'unsafe-file-upload': 1,
|
|
61
|
+
'path-traversal': 2,
|
|
62
|
+
'reflected-xss': 4,
|
|
63
|
+
'sql-injection': 8,
|
|
64
|
+
'cmd-injection': 16,
|
|
65
|
+
'nosql-injection-mongo': 32,
|
|
66
|
+
'bot-blocker': 64,
|
|
67
|
+
'ssjs-injection': 128,
|
|
68
|
+
'method-tampering': 256,
|
|
69
|
+
});
|
|
70
|
+
expect(protect.agentLib).property('MongoQueryType');
|
|
71
|
+
expect(protect.agentLib).property('DbType');
|
|
72
|
+
|
|
73
|
+
expect(protect).property('rules').an('object');
|
|
74
|
+
const { rules } = protect;
|
|
75
|
+
|
|
76
|
+
expect(rules).property('agentLibRules').keys(agentLibRules);
|
|
77
|
+
expect(rules).property('agentLibRulesMask').equal(511);
|
|
78
|
+
expect(rules).property('agentRules').keys(agentRules);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('makeSourceContext() for rules', function() {
|
|
82
|
+
const alRules = ['reflected-xss', 'path-traversal', 'method-tampering'];
|
|
83
|
+
const aRules = ['virtual-patch'];
|
|
84
|
+
const aSlice = aRules.at.length > 1 ? 1 : 0;
|
|
85
|
+
const tests = [
|
|
86
|
+
// config, agent-lib, agent
|
|
87
|
+
{ desc: 'handles no rules', rules: [], alr: [], ar: [] },
|
|
88
|
+
{ desc: 'handles all rules', rules, alr: agentLibRules, ar: agentRules },
|
|
89
|
+
{ desc: 'handles some agent-lib rules', rules: alRules, alr: alRules, ar: [] },
|
|
90
|
+
{ desc: 'handles some agent rules', rules: aRules, alr: [], ar: aRules },
|
|
91
|
+
{ desc: 'handle some of both rules', rules: alRules.concat(aRules), alr: alRules, ar: aRules },
|
|
92
|
+
{ desc: 'handles a single agent-lib rule', rules: alRules.slice(1), alr: alRules.slice(1), ar: [] },
|
|
93
|
+
{ desc: 'handles a single agent rule', rules: aRules.slice(aSlice), alr: [], ar: aRules.slice(aSlice) },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
tests.forEach((t) => {
|
|
97
|
+
it(t.desc, function() {
|
|
98
|
+
const core = mocks.core();
|
|
99
|
+
core.patcher = mocks.patcher();
|
|
100
|
+
core.logger = mocks.logger();
|
|
101
|
+
core.scopes = mocks.scopes();
|
|
102
|
+
core.config = mocks.config();
|
|
103
|
+
// make the rules config based on the test's rules
|
|
104
|
+
core.config.protect.rules = makeProtectRulesConfig(t.rules);
|
|
105
|
+
core.scopes = mocks.scopes();
|
|
106
|
+
|
|
107
|
+
// create protect with the rules config
|
|
108
|
+
require('../../protect')(core);
|
|
109
|
+
const { makeSourceContext } = core.protect;
|
|
110
|
+
const [req, res] = makeReqRes({}, {});
|
|
111
|
+
|
|
112
|
+
// this is what is really being tested.
|
|
113
|
+
const sc = makeSourceContext(req, res);
|
|
114
|
+
|
|
115
|
+
// make expected source context
|
|
116
|
+
const expectedAlr = {};
|
|
117
|
+
let alrm = 0;
|
|
118
|
+
t.alr.forEach((alr) => {
|
|
119
|
+
expectedAlr[alr] = { mode: 'monitor' };
|
|
120
|
+
alrm |= core.protect.agentLib.RuleType[alr];
|
|
121
|
+
});
|
|
122
|
+
const expectedAr = {};
|
|
123
|
+
t.ar.forEach((ar) => expectedAr[ar] = { mode: 'monitor' });
|
|
124
|
+
|
|
125
|
+
// check everything
|
|
126
|
+
expect(sc.block).a('function');
|
|
127
|
+
expect(sc.rules).eql({
|
|
128
|
+
agentLibRules: expectedAlr,
|
|
129
|
+
agentLibRulesMask: alrm,
|
|
130
|
+
agentRules: expectedAr,
|
|
131
|
+
});
|
|
132
|
+
expect(sc.exclusions).eql([]);
|
|
133
|
+
expect(sc.virtualPatches).eql([]);
|
|
134
|
+
expect(sc.findings).eql({
|
|
135
|
+
trackRequest: false,
|
|
136
|
+
securityException: undefined,
|
|
137
|
+
bodyType: undefined,
|
|
138
|
+
resultsMap: Object.create(null),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('makeSourceContext() for req, res', function() {
|
|
145
|
+
const rules = ['reflected-xss'];
|
|
146
|
+
const expectedAlr = { 'reflected-xss': { mode: 'monitor' } };
|
|
147
|
+
const expectedAlrm = 4; // RuleType['reflected-xss']
|
|
148
|
+
const expectedAr = {};
|
|
149
|
+
|
|
150
|
+
const tests = [
|
|
151
|
+
{ desc: 'works with unmodified req', req: {}, res: {} },
|
|
152
|
+
{ desc: 'handles an uppercase header name', req: { rawHeaders: ['Accept', '*'] }, res: {} },
|
|
153
|
+
{ desc: 'does not modify a header value', req: { rawHeaders: ['Accept', 'TEXT/HTmL'] }, res: {} },
|
|
154
|
+
{ desc: 'handles an empty url', req: { url: '' }, res: {} },
|
|
155
|
+
{ desc: 'handles search params', req: { url: '/mimsy?borogroves' }, res: {} },
|
|
156
|
+
{ desc: 'handles only search params', req: { url: '?alice' }, res: {} },
|
|
157
|
+
{ desc: 'detects multipart content-type', req: { rawHeaders: ['Content-type', 'multipart/x-www-form-urlencoded'] }, res: {} },
|
|
158
|
+
{ desc: 'blocks when headers not sent', req: {}, res: { writeHead: sinon.stub(), end: sinon.stub() } },
|
|
159
|
+
{ desc: 'blocks when headers sent', req: {}, res: { writeHead: sinon.stub(), end: sinon.stub(), headersSent: true } },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
tests.forEach((t) => {
|
|
163
|
+
it(t.desc, function() {
|
|
164
|
+
const core = mocks.core();
|
|
165
|
+
core.patcher = mocks.patcher();
|
|
166
|
+
core.logger = mocks.logger();
|
|
167
|
+
core.scopes = mocks.scopes();
|
|
168
|
+
core.config = mocks.config();
|
|
169
|
+
core.config.protect.rules = makeProtectRulesConfig(rules);
|
|
170
|
+
core.scopes = mocks.scopes();
|
|
171
|
+
// create protect with the rules config
|
|
172
|
+
require('../../protect')(core);
|
|
173
|
+
const { makeSourceContext } = core.protect;
|
|
174
|
+
|
|
175
|
+
const [req, res] = makeReqRes(t.req, t.res);
|
|
176
|
+
|
|
177
|
+
// this is what is really being tested.
|
|
178
|
+
const sc = makeSourceContext(req, res);
|
|
179
|
+
|
|
180
|
+
// default to setting headers but not including a content-type
|
|
181
|
+
let contentType = '';
|
|
182
|
+
if (!t.req.rawHeaders) {
|
|
183
|
+
// the test doesn't set the headers so they are the default
|
|
184
|
+
contentType = 'application/x-www-form-urlencoded';
|
|
185
|
+
} else if (t.req.rawHeaders[t.req.rawHeaders.length - 1].startsWith('multipart')) {
|
|
186
|
+
// the test does set the content-type header (inferred)
|
|
187
|
+
contentType = t.req.rawHeaders[t.req.rawHeaders.length - 1];
|
|
188
|
+
}
|
|
189
|
+
const headers = req.rawHeaders.map((h, ix) => ix & 1 ? h : h.toLowerCase());
|
|
190
|
+
const expectedReqData = {
|
|
191
|
+
method: 'get',
|
|
192
|
+
headers,
|
|
193
|
+
uriPath: '/',
|
|
194
|
+
queries: '',
|
|
195
|
+
contentType,
|
|
196
|
+
standardUrlParsing: false,
|
|
197
|
+
};
|
|
198
|
+
if (t.req.rawHeaders) {
|
|
199
|
+
expectedReqData.headers = t.req.rawHeaders.map((h, i) => {
|
|
200
|
+
if (i & 1 && h.startsWith('multipart')) {
|
|
201
|
+
expectedReqData.contentType = h;
|
|
202
|
+
}
|
|
203
|
+
return i & 1 ? h : h.toLowerCase();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (t.req.url !== undefined) {
|
|
207
|
+
expectedReqData.uriPath = t.req.url;
|
|
208
|
+
expectedReqData.queries = '';
|
|
209
|
+
const ix = t.req.url.indexOf('?');
|
|
210
|
+
if (ix >= 0) {
|
|
211
|
+
expectedReqData.uriPath = t.req.url.slice(0, ix);
|
|
212
|
+
expectedReqData.queries = t.req.url.slice(ix + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// check that req and res make the expected abstract request
|
|
217
|
+
expect(sc).property('reqData').eql(expectedReqData);
|
|
218
|
+
|
|
219
|
+
// if a block test, does it work as expected?
|
|
220
|
+
if (t.res.writeHead) {
|
|
221
|
+
sc.block('mode', 'reflected-xss');
|
|
222
|
+
if (!t.res.headersSent) {
|
|
223
|
+
expect(t.res.writeHead.callCount).equal(1);
|
|
224
|
+
} else {
|
|
225
|
+
expect(t.res.writeHead.callCount).equal(0);
|
|
226
|
+
}
|
|
227
|
+
expect(t.res.end.callCount).equal(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// check constant items
|
|
231
|
+
expect(sc.block).a('function');
|
|
232
|
+
expect(sc.rules).eql({
|
|
233
|
+
agentLibRules: expectedAlr,
|
|
234
|
+
agentLibRulesMask: expectedAlrm,
|
|
235
|
+
agentRules: expectedAr,
|
|
236
|
+
});
|
|
237
|
+
expect(sc.exclusions).eql([]);
|
|
238
|
+
expect(sc.virtualPatches).eql([]);
|
|
239
|
+
expect(sc.findings).eql({
|
|
240
|
+
trackRequest: false,
|
|
241
|
+
securityException: undefined,
|
|
242
|
+
bodyType: undefined,
|
|
243
|
+
resultsMap: Object.create(null)
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
//
|
|
252
|
+
// make a req and res pair
|
|
253
|
+
//
|
|
254
|
+
function makeReqRes(req = {}, res = {}) {
|
|
255
|
+
const defaultReq = {
|
|
256
|
+
method: 'get',
|
|
257
|
+
url: '/',
|
|
258
|
+
rawHeaders: getRawHeaders(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const defaultRes = {
|
|
262
|
+
headersSent: false,
|
|
263
|
+
writeHead() {},
|
|
264
|
+
end() {},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return [
|
|
268
|
+
Object.assign({}, defaultReq, req),
|
|
269
|
+
Object.assign({}, defaultRes, res),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getRawHeaders() {
|
|
274
|
+
return [
|
|
275
|
+
'accept', 'text/html',
|
|
276
|
+
'accept-encoding', 'gzip, deflate, br',
|
|
277
|
+
'user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.',
|
|
278
|
+
'content-type', 'application/x-www-form-urlencoded'
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
//
|
|
283
|
+
// create the config for protect's rules
|
|
284
|
+
//
|
|
285
|
+
function makeProtectRulesConfig(rules, mode) {
|
|
286
|
+
if (mode === undefined) {
|
|
287
|
+
mode = 'monitor';
|
|
288
|
+
}
|
|
289
|
+
if (['monitor', 'block', 'off'].indexOf(mode) < 0) {
|
|
290
|
+
throw new Error('valid modes are monitor, block, and off');
|
|
291
|
+
}
|
|
292
|
+
const rulesOptions = {};
|
|
293
|
+
for (const ruleId of rules) {
|
|
294
|
+
rulesOptions[ruleId] = { mode };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return rulesOptions;
|
|
298
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = class SecurityException extends Error {
|
|
4
|
+
static create() {
|
|
5
|
+
const err = new SecurityException('SecurityException');
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static isSecurityException(err) {
|
|
10
|
+
return err instanceof SecurityException;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Domain = require('async-hook-domain');
|
|
4
|
+
const securityException = require('./security-exception');
|
|
5
|
+
|
|
6
|
+
module.exports = function(core) {
|
|
7
|
+
const { logger, protect } = core;
|
|
8
|
+
|
|
9
|
+
function throwSecurityException(sourceContext) {
|
|
10
|
+
if (!sourceContext) return;
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
findings: {
|
|
14
|
+
securityException: [mode, ruleId]
|
|
15
|
+
}
|
|
16
|
+
} = sourceContext;
|
|
17
|
+
|
|
18
|
+
const err = securityException.create();
|
|
19
|
+
|
|
20
|
+
new Domain(() => {
|
|
21
|
+
sourceContext.block(mode, ruleId);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
logger.info({ err, mode, ruleId }, 'throwing security exception');
|
|
25
|
+
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return protect.throwSecurityException = throwSecurityException;
|
|
30
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const proxyquire = require('proxyquire');
|
|
6
|
+
const mocks = require('../../test/mocks');
|
|
7
|
+
|
|
8
|
+
describe('protect throw-security-exception', function() {
|
|
9
|
+
let Domain;
|
|
10
|
+
let core;
|
|
11
|
+
let protect;
|
|
12
|
+
let sourceContext;
|
|
13
|
+
|
|
14
|
+
beforeEach(function() {
|
|
15
|
+
core = mocks.core();
|
|
16
|
+
core.logger = mocks.logger();
|
|
17
|
+
protect = core.protect = mocks.protect();
|
|
18
|
+
sourceContext = {
|
|
19
|
+
block: sinon.stub(),
|
|
20
|
+
findings: {
|
|
21
|
+
securityException: ['block', 'cmd-injection']
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
Domain = sinon.stub().callsFake(function(cb) {
|
|
25
|
+
cb();
|
|
26
|
+
});
|
|
27
|
+
proxyquire('./throw-security-exception', {
|
|
28
|
+
'async-hook-domain': Domain
|
|
29
|
+
})(core);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('domain listener will block in response to thrown exception', function() {
|
|
33
|
+
const { findings: { securityException } } = sourceContext;
|
|
34
|
+
expect(function() {
|
|
35
|
+
protect.throwSecurityException(sourceContext);
|
|
36
|
+
}).to.throw('SecurityException');
|
|
37
|
+
expect(sourceContext.block).calledWith(...securityException);
|
|
38
|
+
expect(core.logger.info).to.have.been.calledWith(sinon.match({
|
|
39
|
+
ruleId: 'cmd-injection',
|
|
40
|
+
mode: 'block',
|
|
41
|
+
}));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('method is noop if source context is not provided', function() {
|
|
45
|
+
expect(function() {
|
|
46
|
+
protect.throwSecurityException();
|
|
47
|
+
expect(sourceContext.block).to.not.have.been.called;
|
|
48
|
+
}).not.to.throw('SecurityException');
|
|
49
|
+
});
|
|
50
|
+
});
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get a symbol parameter value on the given object. The function should be
|
|
5
|
+
* used with a `target` for which we are sure that the Symbol property we need is set before
|
|
6
|
+
* any eventual duplicating Symbol properties. In case of duplicating Symbol properties
|
|
7
|
+
* we will always get the one that's set first.
|
|
8
|
+
* @param {Object} target built outgoing response
|
|
9
|
+
* @param {String} symbolName full symbol stringified
|
|
10
|
+
* @returns {Object} value of the requested symbol property
|
|
11
|
+
*/
|
|
12
|
+
function getSymbolProperty(target, symbolName) {
|
|
13
|
+
if (!target) return;
|
|
14
|
+
for (const sym of Object.getOwnPropertySymbols(target)) {
|
|
15
|
+
if (sym.toString() === `Symbol(${symbolName})`) {
|
|
16
|
+
return target[sym];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* simpleTraverse() walks an object and calls a user function for each key
|
|
23
|
+
* and string value. It is a "simple traverse" in that it
|
|
24
|
+
* 1) doesn't make value callbacks unless the value is a non-empty string
|
|
25
|
+
* 2) it only recognizes items that can be expressed in JSON, i.e., POJO
|
|
26
|
+
* and arrays.
|
|
27
|
+
* 3) it doesn't make callbacks for array indexes (though they appear in
|
|
28
|
+
* the path). array indexes are always numeric and are not a threat.
|
|
29
|
+
*
|
|
30
|
+
* N.B. the path array that is passed to the callback is a dynamic path; new
|
|
31
|
+
* keys are pushed and popped onto the path as simpleTraverse() walks the
|
|
32
|
+
* object. in order to capture the path at the time of the callback, the
|
|
33
|
+
* callback must copy the array, e.g., `path.slice()`, in order to "freeze"
|
|
34
|
+
* it at the time of the callback. the reason for this is that most keys/values
|
|
35
|
+
* are not going to be of interest, and there is no reason to create a new array
|
|
36
|
+
* unless the key/value is of interest.
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} obj the object to traverse
|
|
39
|
+
* @param {Function} cb(path, type, value) is called for each non-array-index key
|
|
40
|
+
* and string value. It is not called for non-string or empty-string Values.
|
|
41
|
+
* path {[String]} the path prior to the 'Key' or 'Value'; includes array indexes.
|
|
42
|
+
* type {String} 'Key' or 'Value'
|
|
43
|
+
* value {String} the Key or Leaf string
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
function simpleTraverse(obj, cb) {
|
|
47
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const path = [];
|
|
51
|
+
/* eslint-disable complexity */
|
|
52
|
+
function traverse(obj) {
|
|
53
|
+
const isArray = Array.isArray(obj);
|
|
54
|
+
for (const k in obj) {
|
|
55
|
+
if (isArray) {
|
|
56
|
+
// if it is an array, store each index in path but don't call the
|
|
57
|
+
// callback on the index itself as they are just numeric strings.
|
|
58
|
+
path.push(k);
|
|
59
|
+
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
60
|
+
traverse(obj[k]);
|
|
61
|
+
} else if (typeof obj[k] === 'string' && obj[k]) {
|
|
62
|
+
cb(path, 'Value', obj[k]);
|
|
63
|
+
}
|
|
64
|
+
path.pop();
|
|
65
|
+
} else if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
66
|
+
cb(path, 'Key', k);
|
|
67
|
+
path.push(k);
|
|
68
|
+
traverse(obj[k]);
|
|
69
|
+
path.pop();
|
|
70
|
+
} else {
|
|
71
|
+
cb(path, 'Key', k);
|
|
72
|
+
// only callback if the value is a non-empty string
|
|
73
|
+
if (typeof obj[k] === 'string' && obj[k]) {
|
|
74
|
+
path.push(k);
|
|
75
|
+
cb(path, 'Value', obj[k]);
|
|
76
|
+
path.pop();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
traverse(obj);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
getSymbolProperty,
|
|
87
|
+
simpleTraverse,
|
|
88
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
|
|
5
|
+
describe('protect utils', function () {
|
|
6
|
+
let getSymbolProperty, target;
|
|
7
|
+
|
|
8
|
+
beforeEach(function() {
|
|
9
|
+
({ getSymbolProperty } = require('./utils'));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns `undefined` if the target is falsey', function() {
|
|
13
|
+
[null, '', undefined].forEach((target) => {
|
|
14
|
+
expect(getSymbolProperty(target, 'test')).to.equal(undefined);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns `undefined` if the target does not have the required symbol', function() {
|
|
19
|
+
target = {
|
|
20
|
+
foo: 'test',
|
|
21
|
+
[Symbol('not foo')]: 'test'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const result = getSymbolProperty(target, 'foo');
|
|
25
|
+
|
|
26
|
+
expect(result).to.equal(undefined);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns value of the required Symbol property', function() {
|
|
30
|
+
target = {
|
|
31
|
+
foo: 'normal property',
|
|
32
|
+
[Symbol('not foo')]: 'another symbol',
|
|
33
|
+
[Symbol('foo')]: 'searched value'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = getSymbolProperty(target, 'foo');
|
|
37
|
+
|
|
38
|
+
expect(result).to.equal('searched value');
|
|
39
|
+
});
|
|
40
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contrast/protect",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Contrast service providing framework-agnostic Protect support",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"types": "lib/index.d.ts",
|
|
12
|
+
"bin": {
|
|
13
|
+
"contrast-transpile": "lib/cli-rewriter.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"npm": ">= 8.4.0",
|
|
17
|
+
"node": ">= 14.15.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "../scripts/test.sh"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@babel/template": "^7.16.7",
|
|
24
|
+
"@babel/types": "^7.16.8",
|
|
25
|
+
"@contrast/agent-lib": "^4.2.0",
|
|
26
|
+
"@contrast/common": "1.0.0",
|
|
27
|
+
"@contrast/core": "1.0.0",
|
|
28
|
+
"@contrast/esm-hooks": "1.0.0",
|
|
29
|
+
"async-hook-domain": "^2.0.4",
|
|
30
|
+
"builtin-modules": "^3.2.0"
|
|
31
|
+
}
|
|
32
|
+
}
|