@contrast/protect 1.14.0 → 1.15.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/lib/index.d.ts +15 -0
- package/lib/input-analysis/handlers.js +86 -11
- package/lib/input-analysis/install/http.js +5 -3
- package/lib/input-tracing/index.js +5 -4
- package/lib/input-tracing/install/fs.js +4 -0
- package/lib/input-tracing/install/mssql.js +79 -0
- package/lib/make-source-context.js +3 -6
- package/package.json +4 -4
package/lib/index.d.ts
CHANGED
|
@@ -42,10 +42,25 @@ export interface ConnectInputs {
|
|
|
42
42
|
queries?: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface ExclusionPolicy {
|
|
46
|
+
exclusions: {
|
|
47
|
+
url: [],
|
|
48
|
+
querystring: [],
|
|
49
|
+
header: [],
|
|
50
|
+
body: [],
|
|
51
|
+
cookie: [],
|
|
52
|
+
parameter: []
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ProtectPolicy = ExclusionPolicy & Record<rule, ProtectRuleMode> & { rulesMask: number };
|
|
57
|
+
|
|
45
58
|
export interface Protect {
|
|
46
59
|
makeResponseBlocker: (res: ServerResponse) => Block,
|
|
47
60
|
makeSourceContext: (req: IncomingMessage, res: ServerResponse) => ProtectRequestStore,
|
|
48
61
|
throwSecurityException: (sourceContext: ProtectRequestStore) => void,
|
|
62
|
+
policy: ProtectPolicy,
|
|
63
|
+
getPolicy(): ProtectPolicy, // creates copy for request scope
|
|
49
64
|
inputAnalysis: {
|
|
50
65
|
handleConnect: (sourceContext: ProtectRequestStore, connectInputs: ConnectInputs) => undefined | [string, string],
|
|
51
66
|
handleRequestEnd: (sourceContext: ProtectRequestStore) => void, //NYI
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const address = require('ipaddr.js');
|
|
18
19
|
const {
|
|
19
20
|
BLOCKING_MODES,
|
|
20
|
-
traverseKeysAndValues,
|
|
21
|
-
traverseValues,
|
|
22
21
|
Rule,
|
|
23
|
-
ProtectRuleMode,
|
|
22
|
+
ProtectRuleMode: { OFF, MONITOR },
|
|
24
23
|
isString,
|
|
25
|
-
|
|
24
|
+
traverseKeysAndValues,
|
|
25
|
+
traverseValues,
|
|
26
|
+
InputType,
|
|
26
27
|
} = require('@contrast/common');
|
|
27
|
-
const address = require('ipaddr.js');
|
|
28
28
|
|
|
29
29
|
//
|
|
30
30
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
@@ -55,6 +55,40 @@ const agentLibRuleTypeToName = {
|
|
|
55
55
|
|
|
56
56
|
const preferWW = { preferWorthWatching: true };
|
|
57
57
|
|
|
58
|
+
const acceptedMethods = new Set([
|
|
59
|
+
'acl',
|
|
60
|
+
'baseline-control',
|
|
61
|
+
'checkin',
|
|
62
|
+
'checkout',
|
|
63
|
+
'connect',
|
|
64
|
+
'copy',
|
|
65
|
+
'delete',
|
|
66
|
+
'get',
|
|
67
|
+
'head',
|
|
68
|
+
'label',
|
|
69
|
+
'lock',
|
|
70
|
+
'merge',
|
|
71
|
+
'mkactivity',
|
|
72
|
+
'mkcalendar',
|
|
73
|
+
'mkcol',
|
|
74
|
+
'mkworkspace',
|
|
75
|
+
'move',
|
|
76
|
+
'options',
|
|
77
|
+
'orderpatch',
|
|
78
|
+
'patch',
|
|
79
|
+
'post',
|
|
80
|
+
'propfind',
|
|
81
|
+
'proppatch',
|
|
82
|
+
'put',
|
|
83
|
+
'report',
|
|
84
|
+
'search',
|
|
85
|
+
'trace',
|
|
86
|
+
'uncheckout',
|
|
87
|
+
'unlock',
|
|
88
|
+
'update',
|
|
89
|
+
'version-control',
|
|
90
|
+
]);
|
|
91
|
+
|
|
58
92
|
module.exports = function(core) {
|
|
59
93
|
const {
|
|
60
94
|
logger,
|
|
@@ -130,16 +164,22 @@ module.exports = function(core) {
|
|
|
130
164
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
131
165
|
const { policy: { rulesMask } } = sourceContext;
|
|
132
166
|
|
|
133
|
-
inputAnalysis.handleVirtualPatches(
|
|
167
|
+
inputAnalysis.handleVirtualPatches(
|
|
168
|
+
sourceContext,
|
|
169
|
+
{ URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
|
|
170
|
+
);
|
|
134
171
|
|
|
135
172
|
// initialize findings to the basics
|
|
136
173
|
let block = undefined;
|
|
137
174
|
if (rulesMask !== 0) {
|
|
138
175
|
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
|
|
139
|
-
|
|
140
176
|
block = mergeFindings(sourceContext, findings);
|
|
141
177
|
}
|
|
142
178
|
|
|
179
|
+
if (!block) {
|
|
180
|
+
block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
return block;
|
|
144
184
|
};
|
|
145
185
|
|
|
@@ -426,19 +466,54 @@ module.exports = function(core) {
|
|
|
426
466
|
}
|
|
427
467
|
};
|
|
428
468
|
|
|
469
|
+
inputAnalysis.handleMethodTampering = function(sourceContext, connectInputs) {
|
|
470
|
+
const ruleId = Rule.METHOD_TAMPERING;
|
|
471
|
+
const mode = sourceContext.policy[ruleId];
|
|
472
|
+
if (mode !== OFF) {
|
|
473
|
+
const { method } = connectInputs;
|
|
474
|
+
|
|
475
|
+
if (!acceptedMethods.has(method)) {
|
|
476
|
+
const result = {
|
|
477
|
+
inputType: InputType.METHOD,
|
|
478
|
+
key: 'method',
|
|
479
|
+
value: method,
|
|
480
|
+
blocked: false,
|
|
481
|
+
exploitMetadata: null,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
sourceContext.resultsMap[ruleId] = [result];
|
|
485
|
+
|
|
486
|
+
if (BLOCKING_MODES.includes(mode)) {
|
|
487
|
+
result.blocked = true;
|
|
488
|
+
return sourceContext.securityException = ['block', ruleId];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
429
494
|
/**
|
|
430
|
-
* handleRequestEnd()
|
|
431
495
|
*
|
|
432
|
-
* Invoked when the request is complete.
|
|
496
|
+
* Invoked when the request is complete. Handles probe analysis (when configured) and
|
|
497
|
+
* various other tasks needed prior to reporting.
|
|
433
498
|
*
|
|
434
499
|
* @param {Object} sourceContext
|
|
435
500
|
*/
|
|
436
501
|
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
502
|
+
{
|
|
503
|
+
// check status code to verify method-tampering exploitation
|
|
504
|
+
const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
|
|
505
|
+
if (mtResult) {
|
|
506
|
+
const { statusCode } = sourceContext.resData;
|
|
507
|
+
if (statusCode !== 405 || statusCode !== 501) {
|
|
508
|
+
mtResult.exploitMetadata = [{ statusCode }];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
437
513
|
if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
|
|
438
514
|
|
|
439
515
|
// Detecting probes
|
|
440
516
|
const { resultsMap, policy: { rulesMask } } = sourceContext;
|
|
441
|
-
|
|
442
517
|
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
443
518
|
const probes = {};
|
|
444
519
|
|
|
@@ -868,5 +943,5 @@ function getValueAtKey(obj, path, key) {
|
|
|
868
943
|
}
|
|
869
944
|
|
|
870
945
|
function isMonitorMode(ruleId, sourceContext) {
|
|
871
|
-
return sourceContext.policy[ruleId] ===
|
|
946
|
+
return sourceContext.policy[ruleId] === MONITOR;
|
|
872
947
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { Event } = require('@contrast/common');
|
|
18
|
+
const { Event, toLowerCase } = require('@contrast/common');
|
|
19
19
|
const { patchType } = require('../constants');
|
|
20
20
|
|
|
21
21
|
module.exports = function(core) {
|
|
@@ -62,10 +62,12 @@ module.exports = function(core) {
|
|
|
62
62
|
|
|
63
63
|
store.protect = core.protect.makeSourceContext(req, res);
|
|
64
64
|
const {
|
|
65
|
-
reqData: { headers, uriPath, method }
|
|
65
|
+
reqData: { headers, uriPath, method },
|
|
66
|
+
resData,
|
|
66
67
|
} = store.protect;
|
|
67
68
|
|
|
68
69
|
res.on('finish', () => {
|
|
70
|
+
resData.statusCode = res.statusCode;
|
|
69
71
|
inputAnalysis.handleRequestEnd(store.protect);
|
|
70
72
|
messages.emit(Event.PROTECT, store);
|
|
71
73
|
});
|
|
@@ -73,7 +75,7 @@ module.exports = function(core) {
|
|
|
73
75
|
const connectInputs = {
|
|
74
76
|
headers: removeCookies(headers),
|
|
75
77
|
uriPath,
|
|
76
|
-
method
|
|
78
|
+
method: toLowerCase(method),
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
if (inputAnalysis.virtualPatchesEvaluators?.length) {
|
|
@@ -25,17 +25,18 @@ module.exports = function(core) {
|
|
|
25
25
|
|
|
26
26
|
// instrumentation
|
|
27
27
|
require('./install/child-process')(core);
|
|
28
|
+
require('./install/eval')(core);
|
|
28
29
|
require('./install/fs')(core);
|
|
29
|
-
require('./install/
|
|
30
|
+
require('./install/function')(core);
|
|
31
|
+
require('./install/http')(core);
|
|
30
32
|
require('./install/marsdb')(core);
|
|
33
|
+
require('./install/mongodb')(core);
|
|
34
|
+
require('./install/mssql')(core);
|
|
31
35
|
require('./install/mysql')(core);
|
|
32
36
|
require('./install/postgres')(core);
|
|
33
37
|
require('./install/sequelize')(core);
|
|
34
38
|
require('./install/sqlite3')(core);
|
|
35
|
-
require('./install/http')(core);
|
|
36
39
|
require('./install/vm')(core);
|
|
37
|
-
require('./install/eval')(core);
|
|
38
|
-
require('./install/function')(core);
|
|
39
40
|
// TODO: NODE-2360 (oracledb)
|
|
40
41
|
|
|
41
42
|
inputTracing.install = function() {
|
|
@@ -80,6 +80,10 @@ module.exports = function(core) {
|
|
|
80
80
|
depHooks.resolve({ name: 'fs' }, fs => {
|
|
81
81
|
fsMethods.forEach(({ method, indices = [0] }) => {
|
|
82
82
|
const name = `fs.${method}`;
|
|
83
|
+
|
|
84
|
+
// may not exist depending on OS
|
|
85
|
+
if (!fs[method]) return;
|
|
86
|
+
|
|
83
87
|
patcher.patch(fs, method, {
|
|
84
88
|
name,
|
|
85
89
|
patchType,
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { isString } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../constants');
|
|
20
|
+
|
|
21
|
+
module.exports = function (core) {
|
|
22
|
+
const {
|
|
23
|
+
depHooks,
|
|
24
|
+
patcher,
|
|
25
|
+
protect,
|
|
26
|
+
protect: { inputTracing },
|
|
27
|
+
} = core;
|
|
28
|
+
|
|
29
|
+
const pre = ({ args, hooked, name, orig }) => {
|
|
30
|
+
const sourceContext = protect.getSourceContext(name);
|
|
31
|
+
const [value] = args;
|
|
32
|
+
if (!sourceContext || !value || !isString(value)) return;
|
|
33
|
+
|
|
34
|
+
const sinkContext = {
|
|
35
|
+
name,
|
|
36
|
+
value,
|
|
37
|
+
stacktraceOpts: {
|
|
38
|
+
constructorOpt: hooked,
|
|
39
|
+
prependFrames: [orig],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
core.protect.inputTracing.mssqlInstrumentation = {
|
|
47
|
+
install() {
|
|
48
|
+
depHooks.resolve(
|
|
49
|
+
{ name: 'mssql', file: 'lib/base/prepared-statement.js' },
|
|
50
|
+
(PreparedStatement) => {
|
|
51
|
+
patcher.patch(PreparedStatement.prototype, 'prepare', {
|
|
52
|
+
name: 'mssql.PreparedStatement.prototype.prepare',
|
|
53
|
+
patchType,
|
|
54
|
+
pre,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
depHooks.resolve(
|
|
60
|
+
{ name: 'mssql', file: 'lib/base/request.js' },
|
|
61
|
+
(Request) => {
|
|
62
|
+
patcher.patch(Request.prototype, 'batch', {
|
|
63
|
+
name: 'mssql.Request.prototype.batch',
|
|
64
|
+
patchType,
|
|
65
|
+
pre,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
patcher.patch(Request.prototype, 'query', {
|
|
69
|
+
name: 'mssql.Request.prototype.query',
|
|
70
|
+
patchType,
|
|
71
|
+
pre,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return core.protect.inputTracing.mssqlInstrumentation;
|
|
79
|
+
};
|
|
@@ -74,19 +74,16 @@ module.exports = function(core) {
|
|
|
74
74
|
contentType,
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
// build the protect object that contains all
|
|
79
|
-
//
|
|
80
77
|
const protectStore = {
|
|
81
78
|
reqData,
|
|
79
|
+
resData: {
|
|
80
|
+
statusCode: null,
|
|
81
|
+
},
|
|
82
82
|
// block closure captures res so it isn't exposed to beyond here
|
|
83
83
|
block: core.protect.makeResponseBlocker(res),
|
|
84
|
-
|
|
85
84
|
policy,
|
|
86
|
-
|
|
87
85
|
exclusions: [],
|
|
88
86
|
virtualPatchesEvaluators: [],
|
|
89
|
-
|
|
90
87
|
trackRequest: false,
|
|
91
88
|
securityException: undefined,
|
|
92
89
|
// bodyType is set to a body type if handlers.parseRawBody() parsed it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Protect support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/agent-lib": "^5.3.4",
|
|
21
|
-
"@contrast/common": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/esm-hooks": "1.
|
|
21
|
+
"@contrast/common": "1.6.0",
|
|
22
|
+
"@contrast/core": "1.13.1",
|
|
23
|
+
"@contrast/esm-hooks": "1.9.1",
|
|
24
24
|
"@contrast/scopes": "1.3.0",
|
|
25
25
|
"ipaddr.js": "^2.0.1",
|
|
26
26
|
"semver": "^7.3.7"
|