@contrast/protect 1.3.0 → 1.5.0
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/lib/error-handlers/index.js +7 -4
- package/lib/error-handlers/install/express4.js +2 -3
- package/lib/error-handlers/install/{fastify3.js → fastify.js} +13 -15
- package/lib/error-handlers/install/hapi.js +75 -0
- package/lib/error-handlers/install/koa2.js +1 -2
- package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
- package/lib/hardening/constants.js +20 -0
- package/lib/hardening/handlers.js +65 -0
- package/lib/hardening/index.js +29 -0
- package/lib/hardening/install/node-serialize0.js +59 -0
- package/lib/index.d.ts +3 -21
- package/lib/index.js +6 -46
- package/lib/input-analysis/handlers.js +198 -39
- package/lib/input-analysis/index.js +9 -6
- package/lib/input-analysis/install/body-parser1.js +20 -18
- package/lib/input-analysis/install/cookie-parser1.js +13 -15
- package/lib/input-analysis/install/express4.js +8 -13
- package/lib/input-analysis/install/fastify.js +86 -0
- package/lib/input-analysis/install/formidable1.js +4 -5
- package/lib/input-analysis/install/hapi.js +106 -0
- package/lib/input-analysis/install/http.js +80 -30
- package/lib/input-analysis/install/koa-body5.js +5 -10
- package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
- package/lib/input-analysis/install/koa2.js +13 -24
- package/lib/input-analysis/install/multer1.js +5 -6
- package/lib/input-analysis/install/qs6.js +7 -11
- package/lib/input-analysis/install/universal-cookie4.js +3 -7
- package/lib/input-analysis/ip-analysis.js +76 -0
- package/lib/input-analysis/virtual-patches.js +109 -0
- package/lib/input-tracing/handlers/index.js +92 -23
- package/lib/input-tracing/index.js +14 -18
- package/lib/input-tracing/install/child-process.js +13 -7
- package/lib/input-tracing/install/eval.js +60 -0
- package/lib/input-tracing/install/fs.js +4 -2
- package/lib/input-tracing/install/function.js +60 -0
- package/lib/input-tracing/install/http.js +63 -0
- package/lib/input-tracing/install/mongodb.js +20 -20
- package/lib/input-tracing/install/mysql.js +3 -2
- package/lib/input-tracing/install/postgres.js +5 -4
- package/lib/input-tracing/install/sequelize.js +7 -5
- package/lib/input-tracing/install/sqlite3.js +6 -4
- package/lib/input-tracing/install/vm.js +132 -0
- package/lib/make-source-context.js +8 -49
- package/lib/policy.js +134 -0
- package/lib/semantic-analysis/handlers.js +161 -0
- package/lib/semantic-analysis/index.js +38 -0
- package/package.json +7 -9
- package/lib/input-analysis/install/fastify3.js +0 -107
- package/lib/utils.js +0 -84
|
@@ -22,7 +22,7 @@ module.exports = (core) => {
|
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
24
|
logger,
|
|
25
|
-
|
|
25
|
+
protect,
|
|
26
26
|
protect: { inputAnalysis },
|
|
27
27
|
} = core;
|
|
28
28
|
|
|
@@ -36,12 +36,11 @@ module.exports = (core) => {
|
|
|
36
36
|
const origCb = data.args[1];
|
|
37
37
|
|
|
38
38
|
function hookedCb(...cbArgs) {
|
|
39
|
-
const sourceContext =
|
|
39
|
+
const sourceContext = protect.getSourceContext('formidable');
|
|
40
|
+
|
|
40
41
|
const [, fields, files] = cbArgs;
|
|
41
42
|
|
|
42
|
-
if (
|
|
43
|
-
logger.debug('source context not available in `formidable` hook');
|
|
44
|
-
} else {
|
|
43
|
+
if (sourceContext) {
|
|
45
44
|
if (fields) {
|
|
46
45
|
sourceContext.parsedBody = fields;
|
|
47
46
|
inputAnalysis.handleParsedBody(sourceContext, fields);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { patchType } = require('../constants');
|
|
19
|
+
const { isSecurityException } = require('../../security-exception');
|
|
20
|
+
|
|
21
|
+
module.exports = (core) => {
|
|
22
|
+
const {
|
|
23
|
+
depHooks,
|
|
24
|
+
patcher,
|
|
25
|
+
logger,
|
|
26
|
+
protect,
|
|
27
|
+
protect: { inputAnalysis },
|
|
28
|
+
} = core;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* registers a depHook for hapi module instrumentation
|
|
32
|
+
*/
|
|
33
|
+
function install() {
|
|
34
|
+
depHooks.resolve(
|
|
35
|
+
{ name: 'hapi', version: '>=18 <21' },
|
|
36
|
+
registerServerHandler
|
|
37
|
+
);
|
|
38
|
+
depHooks.resolve(
|
|
39
|
+
{ name: '@hapi/hapi', version: '>=18 <21' },
|
|
40
|
+
registerServerHandler
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const registerServerHandler = (hapi) => {
|
|
45
|
+
patcher.patch(hapi, 'server', {
|
|
46
|
+
name: 'hapi.server',
|
|
47
|
+
patchType,
|
|
48
|
+
post(data) {
|
|
49
|
+
const server = data.result;
|
|
50
|
+
if (server) {
|
|
51
|
+
instrumentServer(server);
|
|
52
|
+
} else {
|
|
53
|
+
logger.error('Hapi Server is called but there is no server instance!');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const instrumentServer = (server) => {
|
|
60
|
+
server.ext('onPreStart', function onPreStart(core) {
|
|
61
|
+
logger.info('hapi version %s', core.version);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
server.ext('onPreHandler', function onPreHandler(req, h) {
|
|
65
|
+
const sourceContext = protect.getSourceContext('hapi.onPreHandler');
|
|
66
|
+
|
|
67
|
+
if (sourceContext) {
|
|
68
|
+
try {
|
|
69
|
+
if (req.params && Object.keys(req.params).length) {
|
|
70
|
+
sourceContext.parsedParams = req.params;
|
|
71
|
+
inputAnalysis.handleUrlParams(sourceContext, req.params);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (req.cookies && Object.keys(req.cookies).length) {
|
|
75
|
+
sourceContext.parsedCookies = req.cookies;
|
|
76
|
+
inputAnalysis.handleCookies(sourceContext, req.cookies);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (req.payload && Object.keys(req.payload).length) {
|
|
80
|
+
sourceContext.parsedBody = req.payload;
|
|
81
|
+
inputAnalysis.handleParsedBody(sourceContext, req.payload);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (req.query && Object.keys(req.query).length) {
|
|
85
|
+
sourceContext.parsedQuery = req.query;
|
|
86
|
+
inputAnalysis.handleQueryParams(sourceContext, req.query);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (isSecurityException(err)) {
|
|
90
|
+
throw err;
|
|
91
|
+
} else {
|
|
92
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return h.continue;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const hapiInstrumentation = inputAnalysis.hapiInstrumentation = {
|
|
102
|
+
install
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return hapiInstrumentation;
|
|
106
|
+
};
|
|
@@ -33,8 +33,8 @@ class HttpInstrumentation {
|
|
|
33
33
|
this.config = core.config;
|
|
34
34
|
this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
|
|
35
35
|
this.depHooks = core.depHooks;
|
|
36
|
-
this.messages = core.messages;
|
|
37
36
|
this.protect = core.protect;
|
|
37
|
+
this.patcher = core.patcher;
|
|
38
38
|
this.makeSourceContext = this.protect.makeSourceContext;
|
|
39
39
|
this.maxBodySize = 16 * 1024 * 1024;
|
|
40
40
|
this.installed = false;
|
|
@@ -52,6 +52,7 @@ class HttpInstrumentation {
|
|
|
52
52
|
this.installed = true;
|
|
53
53
|
this.hookHttp();
|
|
54
54
|
this.hookHttps();
|
|
55
|
+
this.hookHttp2();
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
uninstall() {
|
|
@@ -63,7 +64,7 @@ class HttpInstrumentation {
|
|
|
63
64
|
*/
|
|
64
65
|
hookHttp() {
|
|
65
66
|
this.logger.debug('hooking library: http');
|
|
66
|
-
this.depHooks.resolve({ name: 'http' }, this.
|
|
67
|
+
this.depHooks.resolve({ name: 'http' }, (http) => this.hookServerEmit.call(this, http, 'httpServer'));
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -71,36 +72,78 @@ class HttpInstrumentation {
|
|
|
71
72
|
*/
|
|
72
73
|
hookHttps() {
|
|
73
74
|
this.logger.debug('hooking library: https');
|
|
74
|
-
this.depHooks.resolve({ name: 'https' }, this.
|
|
75
|
+
this.depHooks.resolve({ name: 'https' }, (https) => this.hookServerEmit.call(this, https, 'httpsServer'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sets hooks to instrument `http2 Servers`.
|
|
80
|
+
*/
|
|
81
|
+
hookHttp2() {
|
|
82
|
+
this.logger.debug('hooking library: http2');
|
|
83
|
+
// http2 library does not expose its Server class, so we need to hook the createServer function
|
|
84
|
+
this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2Server'));
|
|
85
|
+
this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2SecureServer', 'createSecureServer'));
|
|
86
|
+
|
|
87
|
+
this.logger.debug('hooking library: spdy');
|
|
88
|
+
this.depHooks.resolve({ name: 'spdy' }, (spdy) => this.hookServerEmit.call(this, spdy, 'spdyServer'));
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
|
-
* Instruments the `Server` prototype from `http(s)
|
|
92
|
+
* Instruments the `Server` prototype from `http(s)` or spdy's http2 Server. This patches `emit` and
|
|
79
93
|
* invokes the protect service to do analysis when appropriate.
|
|
80
|
-
*
|
|
81
|
-
* @param {Object} xport The http(s) module export
|
|
82
94
|
*/
|
|
83
|
-
|
|
95
|
+
hookServerEmit(serverSource, sourceName) {
|
|
96
|
+
serverSource.Server.prototype = this.patcher.patch(serverSource.Server.prototype, 'emit', {
|
|
97
|
+
name: `${sourceName}.Server.prototype.emit`,
|
|
98
|
+
patchType: 'initiate-handling',
|
|
99
|
+
around: this.emitAroundHook.bind(this)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Instruments the `Http2Server` prototype which results from the http2.createServer/createSecureServer() call.
|
|
105
|
+
* This also patches `emit` and
|
|
106
|
+
* invokes the protect service to do analysis when appropriate.
|
|
107
|
+
*/
|
|
108
|
+
hookCreateServer(serverSource, sourceName, constructorName = 'createServer') {
|
|
84
109
|
const self = this;
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
return this.patcher.patch(serverSource, constructorName, {
|
|
112
|
+
name: sourceName,
|
|
113
|
+
patchType: 'initiate-handling',
|
|
114
|
+
post(data) {
|
|
115
|
+
|
|
116
|
+
const { result: server } = data;
|
|
117
|
+
const serverPrototype = server ? Object.getPrototypeOf(server) : null;
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
|
|
119
|
+
if (!serverPrototype) {
|
|
120
|
+
self.logger.error('Unable to patch server prototype, continue without instrumentation');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
94
123
|
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
self.patcher.patch(serverPrototype, 'emit', {
|
|
125
|
+
name: `${sourceName}.Server.prototype.emit`,
|
|
126
|
+
patchType: 'req-async-storage',
|
|
127
|
+
around: self.emitAroundHook.bind(self)
|
|
128
|
+
});
|
|
97
129
|
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
98
132
|
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The around hook for `emit` that
|
|
135
|
+
* invokes the protect service to do analysis when appropriate.
|
|
136
|
+
*/
|
|
137
|
+
emitAroundHook(next, data) {
|
|
138
|
+
const [type] = data.args;
|
|
139
|
+
|
|
140
|
+
if (type !== 'request') {
|
|
141
|
+
return next();
|
|
142
|
+
}
|
|
101
143
|
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
const context = { instance: data.obj, method: next, args: data.args };
|
|
145
|
+
this.initiateRequestHandling(context);
|
|
146
|
+
return !!data.obj._events[type];
|
|
104
147
|
}
|
|
105
148
|
|
|
106
149
|
/**
|
|
@@ -118,15 +161,6 @@ class HttpInstrumentation {
|
|
|
118
161
|
args: [, req, res]
|
|
119
162
|
} = fnContext;
|
|
120
163
|
|
|
121
|
-
// URL exclusions should be applied here. there is no point in doing any additional
|
|
122
|
-
// work if the url is excluded for a particular rule, i.e., that rule should be removed
|
|
123
|
-
// from the list of rules for this request. and if all rules are excluded for this url
|
|
124
|
-
// then none of the following needs to be done.
|
|
125
|
-
if (this.protect.rules.agentLibRulesMask === 0) {
|
|
126
|
-
this.logger.debug('no agent-lib rules are enabled, not checking request');
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
164
|
let store;
|
|
131
165
|
let block;
|
|
132
166
|
|
|
@@ -137,8 +171,10 @@ class HttpInstrumentation {
|
|
|
137
171
|
// so that an async context is present.
|
|
138
172
|
store = this.scope.getStore();
|
|
139
173
|
// nothing can be done if async context is not available.
|
|
174
|
+
|
|
140
175
|
if (!store) {
|
|
141
176
|
this.logger.debug('cannot acquire store for initiateRequestHandling()');
|
|
177
|
+
setImmediate(() => method.call(instance, ...args));
|
|
142
178
|
return;
|
|
143
179
|
}
|
|
144
180
|
|
|
@@ -166,17 +202,31 @@ class HttpInstrumentation {
|
|
|
166
202
|
const connectInputs = {
|
|
167
203
|
headers: HttpInstrumentation.removeCookies(reqData.headers),
|
|
168
204
|
uriPath: reqData.uriPath,
|
|
205
|
+
rawUrl: req.url,
|
|
169
206
|
// TODO AGENT-203 - need to handle method-tampering rule.
|
|
170
207
|
method: reqData.method,
|
|
171
208
|
};
|
|
209
|
+
|
|
172
210
|
// only add queries if it's known that 'qs' or equivalent won't be used.
|
|
173
211
|
/* c8 ignore next 3 */
|
|
174
212
|
if (reqData.standardUrlParsing) {
|
|
175
213
|
connectInputs.queries = reqData.queries;
|
|
176
214
|
}
|
|
177
215
|
|
|
178
|
-
|
|
216
|
+
if (inputAnalysis.virtualPatchesEvaluators?.length) {
|
|
217
|
+
store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (inputAnalysis.ipDenylist?.length) {
|
|
221
|
+
block = inputAnalysis.handleIpDenylist(store.protect, inputAnalysis.ipDenylist);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (inputAnalysis.ipAllowlist?.length) {
|
|
225
|
+
const allowed = inputAnalysis.handleIpAllowlist(store.protect, inputAnalysis.ipAllowlist);
|
|
226
|
+
if (!block) Object.assign(store.protect, { allowed });
|
|
227
|
+
}
|
|
179
228
|
|
|
229
|
+
block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
|
|
180
230
|
} catch (err) {
|
|
181
231
|
this.logger.error({ err }, 'Error during input analysis');
|
|
182
232
|
}
|
|
@@ -21,8 +21,7 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
|
|
25
|
-
scopes: { sources },
|
|
24
|
+
protect,
|
|
26
25
|
protect: { inputAnalysis },
|
|
27
26
|
} = core;
|
|
28
27
|
|
|
@@ -39,15 +38,11 @@ module.exports = (core) => {
|
|
|
39
38
|
const [ctx, origNext] = data.args;
|
|
40
39
|
|
|
41
40
|
async function contrastNext(origErr) {
|
|
42
|
-
const sourceContext =
|
|
41
|
+
const sourceContext = protect.getSourceContext('koa-body');
|
|
43
42
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (ctx.request.body && Object.keys(ctx.request.body).length) {
|
|
48
|
-
sourceContext.parsedBody = ctx.request.body;
|
|
49
|
-
inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
|
|
50
|
-
}
|
|
43
|
+
if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
|
|
44
|
+
sourceContext.parsedBody = ctx.request.body;
|
|
45
|
+
inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
await origNext(origErr);
|
|
@@ -21,8 +21,7 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
|
|
25
|
-
scopes: { sources },
|
|
24
|
+
protect,
|
|
26
25
|
protect: { inputAnalysis },
|
|
27
26
|
} = core;
|
|
28
27
|
|
|
@@ -39,15 +38,12 @@ module.exports = (core) => {
|
|
|
39
38
|
const [ctx, origNext] = data.args;
|
|
40
39
|
|
|
41
40
|
async function contrastNext(origErr) {
|
|
42
|
-
const sourceContext =
|
|
41
|
+
const sourceContext = protect.getSourceContext('koa-bodyparser');
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
sourceContext.parsedBody = ctx.request.body;
|
|
49
|
-
inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
|
|
50
|
-
}
|
|
43
|
+
|
|
44
|
+
if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
|
|
45
|
+
sourceContext.parsedBody = ctx.request.body;
|
|
46
|
+
inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
await origNext(origErr);
|
|
@@ -26,8 +26,7 @@ module.exports = (core) => {
|
|
|
26
26
|
const {
|
|
27
27
|
depHooks,
|
|
28
28
|
patcher,
|
|
29
|
-
|
|
30
|
-
scopes: { sources },
|
|
29
|
+
protect,
|
|
31
30
|
protect: { inputAnalysis },
|
|
32
31
|
} = core;
|
|
33
32
|
|
|
@@ -38,11 +37,9 @@ module.exports = (core) => {
|
|
|
38
37
|
depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
|
|
39
38
|
function contrastStartMiddleware(ctx, next) {
|
|
40
39
|
if (ctx.query && Object.keys(ctx.query).length) {
|
|
41
|
-
const sourceContext =
|
|
40
|
+
const sourceContext = protect.getSourceContext('Koa startMiddleware');
|
|
42
41
|
|
|
43
|
-
if (!sourceContext) {
|
|
44
|
-
logger.debug('source context not available in `qs` hook');
|
|
45
|
-
} else if (!('parsedQuery' in sourceContext)) {
|
|
42
|
+
if (sourceContext && !('parsedQuery' in sourceContext)) {
|
|
46
43
|
sourceContext.parsedQuery = ctx.query;
|
|
47
44
|
inputAnalysis.handleQueryParams(sourceContext, ctx.query);
|
|
48
45
|
}
|
|
@@ -76,15 +73,11 @@ module.exports = (core) => {
|
|
|
76
73
|
name: `[${router}].layer.prototype`,
|
|
77
74
|
patchType,
|
|
78
75
|
post({ result }) {
|
|
79
|
-
const sourceContext =
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (Object.keys(result).length) {
|
|
85
|
-
sourceContext.parsedParams = result;
|
|
86
|
-
inputAnalysis.handleUrlParams(sourceContext, result);
|
|
87
|
-
}
|
|
76
|
+
const sourceContext = protect.getSourceContext(`[${router}].layer`);
|
|
77
|
+
|
|
78
|
+
if (sourceContext && Object.keys(result).length) {
|
|
79
|
+
sourceContext.parsedParams = result;
|
|
80
|
+
inputAnalysis.handleUrlParams(sourceContext, result);
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
83
|
});
|
|
@@ -107,15 +100,11 @@ module.exports = (core) => {
|
|
|
107
100
|
const [ctx, origNext] = data.args;
|
|
108
101
|
|
|
109
102
|
async function contrastNext(origErr) {
|
|
110
|
-
const sourceContext =
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (ctx.cookie) {
|
|
116
|
-
sourceContext.parsedCookies = ctx.cookie;
|
|
117
|
-
inputAnalysis.handleCookies(sourceContext, ctx.cookie);
|
|
118
|
-
}
|
|
103
|
+
const sourceContext = protect.getSourceContext('koa-cookie');
|
|
104
|
+
|
|
105
|
+
if (sourceContext && ctx.cookie) {
|
|
106
|
+
sourceContext.parsedCookies = ctx.cookie;
|
|
107
|
+
inputAnalysis.handleCookies(sourceContext, ctx.cookie);
|
|
119
108
|
}
|
|
120
109
|
|
|
121
110
|
await origNext(origErr);
|
|
@@ -23,7 +23,8 @@ module.exports = (core) => {
|
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
25
|
logger,
|
|
26
|
-
scopes: {
|
|
26
|
+
scopes: { wrap },
|
|
27
|
+
protect,
|
|
27
28
|
protect: { inputAnalysis },
|
|
28
29
|
} = core;
|
|
29
30
|
|
|
@@ -41,11 +42,9 @@ module.exports = (core) => {
|
|
|
41
42
|
|
|
42
43
|
// We are getting the sourceContext here because in the time of calling
|
|
43
44
|
// the contrastNext() method the context is lost
|
|
44
|
-
const sourceContext =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
45
|
+
const sourceContext = protect.getSourceContext('multer');
|
|
46
|
+
|
|
47
|
+
if (!sourceContext) return;
|
|
49
48
|
|
|
50
49
|
function contrastNext(origErr) {
|
|
51
50
|
let securityException;
|
|
@@ -21,8 +21,7 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
|
|
25
|
-
scopes: { sources },
|
|
24
|
+
protect,
|
|
26
25
|
protect: { inputAnalysis },
|
|
27
26
|
} = core;
|
|
28
27
|
|
|
@@ -34,16 +33,13 @@ module.exports = (core) => {
|
|
|
34
33
|
patchType,
|
|
35
34
|
post({ args, result }) {
|
|
36
35
|
if (result && Object.keys(result).length) {
|
|
37
|
-
const sourceContext =
|
|
36
|
+
const sourceContext = protect.getSourceContext('qs');
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// the respective library that's using it (e.g. `formidable`, `co-body`) because in
|
|
45
|
-
// some cases its use is optional and we cannot rely on it.
|
|
46
|
-
} else if (sourceContext.reqData?.queries === args[0]) {
|
|
38
|
+
// We need to run analysis for the `qs` result only when it's used as a query parser.
|
|
39
|
+
// `qs` is used also for parsing bodies, but these cases we handle individually with
|
|
40
|
+
// the respective library that's using it (e.g. `formidable`, `co-body`) because in
|
|
41
|
+
// some cases its use is optional and we cannot rely on it.
|
|
42
|
+
if (sourceContext && sourceContext.reqData?.queries === args[0]) {
|
|
47
43
|
sourceContext.parsedQuery = result;
|
|
48
44
|
inputAnalysis.handleQueryParams(sourceContext, result);
|
|
49
45
|
}
|
|
@@ -21,12 +21,10 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
|
|
25
|
-
scopes: { sources },
|
|
24
|
+
protect,
|
|
26
25
|
protect: { inputAnalysis },
|
|
27
26
|
} = core;
|
|
28
27
|
|
|
29
|
-
|
|
30
28
|
// Patch `universal-cookie` package
|
|
31
29
|
function install() {
|
|
32
30
|
depHooks.resolve({ name: 'universal-cookie', file: 'cjs/utils.js' }, (uCookieUtils) => patcher.patch(uCookieUtils, 'parseCookies', {
|
|
@@ -34,11 +32,9 @@ module.exports = (core) => {
|
|
|
34
32
|
patchType,
|
|
35
33
|
post({ result }) {
|
|
36
34
|
if (result && Object.keys(result).length) {
|
|
37
|
-
const sourceContext =
|
|
35
|
+
const sourceContext = protect.getSourceContext('universal-cookie');
|
|
38
36
|
|
|
39
|
-
if (
|
|
40
|
-
logger.debug('source context not available in `universal-cookie` hook');
|
|
41
|
-
} else {
|
|
37
|
+
if (sourceContext) {
|
|
42
38
|
sourceContext.parsedCookies = result;
|
|
43
39
|
inputAnalysis.handleCookies(sourceContext, result);
|
|
44
40
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { Event } = require('@contrast/common');
|
|
19
|
+
const address = require('ipaddr.js');
|
|
20
|
+
|
|
21
|
+
module.exports = (core) => {
|
|
22
|
+
const {
|
|
23
|
+
messages,
|
|
24
|
+
protect: { inputAnalysis },
|
|
25
|
+
} = core;
|
|
26
|
+
|
|
27
|
+
const ipAllowlist = inputAnalysis.ipAllowlist = [];
|
|
28
|
+
const ipDenylist = inputAnalysis.ipDenylist = [];
|
|
29
|
+
|
|
30
|
+
messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
|
|
31
|
+
const now = new Date().getTime();
|
|
32
|
+
const updatedIpAllowList = serverUpdate.features?.defend?.ipAllowlist.map((ipEntry) => ipEntryMap(ipEntry, now));
|
|
33
|
+
const updatedIpDenyList = serverUpdate.features?.defend?.ipDenylist.map((ipEntry) => ipEntryMap(ipEntry, now));
|
|
34
|
+
|
|
35
|
+
if (updatedIpAllowList) {
|
|
36
|
+
ipAllowlist.length = 0;
|
|
37
|
+
ipAllowlist.push(...updatedIpAllowList);
|
|
38
|
+
}
|
|
39
|
+
if (updatedIpDenyList) {
|
|
40
|
+
ipDenylist.length = 0;
|
|
41
|
+
ipDenylist.push(...updatedIpDenyList);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function ipEntryMap(ipEntry, startTime) {
|
|
47
|
+
const { ip, expires } = ipEntry;
|
|
48
|
+
let doesExpire, expiresAt, cidr;
|
|
49
|
+
|
|
50
|
+
if (expires) {
|
|
51
|
+
doesExpire = true;
|
|
52
|
+
expiresAt = startTime + expires;
|
|
53
|
+
} else {
|
|
54
|
+
doesExpire = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const slashIdx = ip.indexOf('/');
|
|
58
|
+
const isCIDR = slashIdx >= 0;
|
|
59
|
+
const ipInstance = isCIDR
|
|
60
|
+
? address.process(ip.substr(0, slashIdx))
|
|
61
|
+
: address.process(ip);
|
|
62
|
+
|
|
63
|
+
const normalizedValue = ipInstance.toNormalizedString();
|
|
64
|
+
|
|
65
|
+
if (isCIDR) {
|
|
66
|
+
cidr = { range: address.parseCIDR(ip), kind: ipInstance.kind() };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...ipEntry,
|
|
71
|
+
doesExpire,
|
|
72
|
+
expiresAt,
|
|
73
|
+
normalizedValue,
|
|
74
|
+
cidr
|
|
75
|
+
};
|
|
76
|
+
}
|