@contrast/protect 1.12.2 → 1.13.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/common-handler.js +40 -0
- package/lib/error-handlers/index.js +8 -5
- package/lib/error-handlers/init-domain.js +61 -0
- package/lib/index.d.ts +5 -10
- package/lib/index.js +1 -1
- package/lib/input-analysis/install/http.js +25 -9
- package/lib/input-tracing/handlers/index.js +64 -65
- package/lib/input-tracing/install/mysql.js +6 -1
- package/package.json +9 -6
|
@@ -0,0 +1,40 @@
|
|
|
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 { isSecurityException } = require('../security-exception');
|
|
19
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const {
|
|
22
|
+
logger,
|
|
23
|
+
protect: { getSourceContext },
|
|
24
|
+
} = core;
|
|
25
|
+
|
|
26
|
+
return core.protect.errorHandlers.commonHandler = function(err) {
|
|
27
|
+
if (!isSecurityException(err)) {
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sourceContext = getSourceContext('protect-common-error-handler');
|
|
32
|
+
if (!sourceContext) {
|
|
33
|
+
logger.info('SecurityException caught by Contrast but Protect store is unavailable for req handling');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const blockInfo = sourceContext.securityException;
|
|
38
|
+
sourceContext.block(...blockInfo);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -15,20 +15,23 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
18
20
|
module.exports = function(core) {
|
|
19
21
|
const errorHandlers = core.protect.errorHandlers = {};
|
|
20
22
|
|
|
23
|
+
// api
|
|
24
|
+
require('./common-handler')(core);
|
|
25
|
+
require('./init-domain')(core);
|
|
26
|
+
|
|
27
|
+
// installers
|
|
21
28
|
require('./install/fastify')(core);
|
|
22
29
|
require('./install/koa2')(core);
|
|
23
30
|
require('./install/express4')(core);
|
|
24
31
|
require('./install/hapi')(core);
|
|
25
32
|
|
|
26
33
|
errorHandlers.install = function() {
|
|
27
|
-
|
|
28
|
-
if (component.install) {
|
|
29
|
-
component.install();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
34
|
+
callChildComponentMethodsSync(errorHandlers, 'install');
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
return errorHandlers;
|
|
@@ -0,0 +1,61 @@
|
|
|
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 process = require('process');
|
|
19
|
+
const semver = require('semver');
|
|
20
|
+
|
|
21
|
+
module.exports = function(core) {
|
|
22
|
+
const {
|
|
23
|
+
logger,
|
|
24
|
+
protect: { errorHandlers }
|
|
25
|
+
} = core;
|
|
26
|
+
|
|
27
|
+
Object.assign(errorHandlers, initSupportedDomainPackage());
|
|
28
|
+
|
|
29
|
+
return errorHandlers.initDomain = function(req, res) {
|
|
30
|
+
const { AsyncHookDomain, Domain } = errorHandlers;
|
|
31
|
+
|
|
32
|
+
if (AsyncHookDomain) {
|
|
33
|
+
new AsyncHookDomain(errorHandlers.commonHandler);
|
|
34
|
+
} else {
|
|
35
|
+
const domain = new Domain();
|
|
36
|
+
domain.add(req);
|
|
37
|
+
domain.add(res);
|
|
38
|
+
domain.on('error', errorHandlers.commonHandler);
|
|
39
|
+
return domain;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function initSupportedDomainPackage() {
|
|
44
|
+
let AsyncHookDomain, Domain;
|
|
45
|
+
|
|
46
|
+
if (semver.lt(process.version, '16.0.0')) {
|
|
47
|
+
logger.info(
|
|
48
|
+
'%s. %s. %s.',
|
|
49
|
+
'falling back to deprecated \'domain\' module for async SecurityException handling',
|
|
50
|
+
'upgrade to Node 16 LTS or above to allow use of \'async-hook-domain\' modern alternative',
|
|
51
|
+
'upgrading will resolve any deprecation warnings and prevent the logging of this message'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Domain = require('domain').Domain;
|
|
55
|
+
} else {
|
|
56
|
+
AsyncHookDomain = require('async-hook-domain');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { AsyncHookDomain, Domain };
|
|
60
|
+
}
|
|
61
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -13,14 +13,11 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import { Sources } from '@contrast/scopes';
|
|
18
|
-
import RequireHook from '@contrast/require-hook';
|
|
19
|
-
import { RulesConfig, Messages, ReqData, ProtectMessage, ResultMap, ProtectRuleMode } from '@contrast/common';
|
|
16
|
+
import { ReqData, ProtectMessage, ResultMap, ProtectRuleMode } from '@contrast/common';
|
|
20
17
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
21
|
-
import { Config } from '@contrast/config';
|
|
22
18
|
import * as http from 'node:http';
|
|
23
19
|
import * as https from 'node:https';
|
|
20
|
+
import { Domain } from 'domain';
|
|
24
21
|
|
|
25
22
|
type Http = typeof http;
|
|
26
23
|
type Https = typeof https;
|
|
@@ -29,11 +26,7 @@ export type Block = (mode: string, ruleId: string) => void;
|
|
|
29
26
|
export interface ProtectRequestStore {
|
|
30
27
|
reqData: ReqData;
|
|
31
28
|
block: Block;
|
|
32
|
-
rules: {
|
|
33
|
-
agentLibRules: RulesConfig;
|
|
34
|
-
agentLibRulesMask: number;
|
|
35
|
-
agentRules: RulesConfig;
|
|
36
|
-
};
|
|
29
|
+
rules: Record<Rule, { mode: ProtectRuleMode }>;
|
|
37
30
|
exclusions: any[]; // TODO
|
|
38
31
|
virtualPatches: any[]; // TODO
|
|
39
32
|
trackRequest: boolean;
|
|
@@ -105,6 +98,8 @@ export interface Protect {
|
|
|
105
98
|
install: () => void
|
|
106
99
|
}
|
|
107
100
|
errorHandlers: {
|
|
101
|
+
commonHandler: (err: Error) => void;
|
|
102
|
+
initDomain: () => void | Domain;
|
|
108
103
|
fastify3ErrorHandler: {
|
|
109
104
|
_userHandler: null | ((...args: any[]) => any),
|
|
110
105
|
defaultErrorHandler: (error: Error, request: IncomingMessage, reply: ServerResponse) => void,
|
package/lib/index.js
CHANGED
|
@@ -28,11 +28,11 @@ module.exports = function(core) {
|
|
|
28
28
|
require('./make-response-blocker')(core);
|
|
29
29
|
require('./make-source-context')(core);
|
|
30
30
|
require('./get-source-context')(core);
|
|
31
|
+
require('./error-handlers')(core);
|
|
31
32
|
require('./input-analysis')(core);
|
|
32
33
|
require('./input-tracing')(core);
|
|
33
34
|
require('./hardening')(core);
|
|
34
35
|
require('./semantic-analysis')(core);
|
|
35
|
-
require('./error-handlers')(core);
|
|
36
36
|
|
|
37
37
|
protect.install = function() {
|
|
38
38
|
callChildComponentMethodsSync(protect, 'install');
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { patchType } = require('../constants');
|
|
19
18
|
const { Event } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../constants');
|
|
20
20
|
|
|
21
21
|
module.exports = function(core) {
|
|
22
22
|
const {
|
|
@@ -24,9 +24,17 @@ module.exports = function(core) {
|
|
|
24
24
|
messages,
|
|
25
25
|
scopes: { sources },
|
|
26
26
|
instrumentation: { instrument },
|
|
27
|
-
protect: {
|
|
27
|
+
protect: {
|
|
28
|
+
inputAnalysis,
|
|
29
|
+
errorHandlers: { initDomain }
|
|
30
|
+
},
|
|
28
31
|
} = core;
|
|
29
32
|
|
|
33
|
+
const instr = inputAnalysis.httpInstrumentation = {
|
|
34
|
+
install,
|
|
35
|
+
around
|
|
36
|
+
};
|
|
37
|
+
|
|
30
38
|
function removeCookies(headers) {
|
|
31
39
|
for (let i = 0; i < headers.length; i += 2) {
|
|
32
40
|
if (headers[i] === 'cookies') {
|
|
@@ -48,7 +56,7 @@ module.exports = function(core) {
|
|
|
48
56
|
try {
|
|
49
57
|
store = sources.getStore();
|
|
50
58
|
if (!store) {
|
|
51
|
-
logger.debug('
|
|
59
|
+
logger.debug('request store not available during http input-analysis');
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -83,11 +91,21 @@ module.exports = function(core) {
|
|
|
83
91
|
|
|
84
92
|
block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
|
|
85
93
|
} catch (err) {
|
|
86
|
-
logger.error({ err }, 'Error during input analysis');
|
|
94
|
+
logger.error({ err }, 'Error during http input analysis');
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
if (!block) {
|
|
90
|
-
setImmediate(() =>
|
|
98
|
+
setImmediate(() => {
|
|
99
|
+
const domain = initDomain(req, res);
|
|
100
|
+
|
|
101
|
+
if (domain) {
|
|
102
|
+
domain.run(() => {
|
|
103
|
+
next.call(data.obj, ...data.args);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
next.call(data.obj, ...data.args);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
91
109
|
} else {
|
|
92
110
|
store.protect.block(...block);
|
|
93
111
|
logger.debug({ block }, 'request blocked by not emitting request event');
|
|
@@ -129,6 +147,7 @@ module.exports = function(core) {
|
|
|
129
147
|
around
|
|
130
148
|
}
|
|
131
149
|
];
|
|
150
|
+
|
|
132
151
|
instrument({
|
|
133
152
|
moduleName,
|
|
134
153
|
patchObjects
|
|
@@ -136,8 +155,5 @@ module.exports = function(core) {
|
|
|
136
155
|
});
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
return
|
|
140
|
-
install,
|
|
141
|
-
around
|
|
142
|
-
};
|
|
158
|
+
return instr;
|
|
143
159
|
};
|
|
@@ -73,13 +73,16 @@ module.exports = function(core) {
|
|
|
73
73
|
|
|
74
74
|
for (const result of results) {
|
|
75
75
|
let findings = null;
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let inputIndex = sinkContext.value.indexOf(result.value);
|
|
77
|
+
|
|
78
|
+
while (!findings && inputIndex >= 0) {
|
|
78
79
|
findings = agentLib.checkCommandInjectionSink(
|
|
79
80
|
inputIndex,
|
|
80
81
|
result.value.length,
|
|
81
82
|
sinkContext.value,
|
|
82
83
|
);
|
|
84
|
+
|
|
85
|
+
inputIndex = sinkContext.value.indexOf(result.value, inputIndex + 1);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
if (findings) {
|
|
@@ -96,28 +99,26 @@ module.exports = function(core) {
|
|
|
96
99
|
|
|
97
100
|
for (const result of results) {
|
|
98
101
|
let findings = null;
|
|
102
|
+
let inputIndex = sinkContext.value.indexOf(result.value);
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
while (!findings && inputIndex >= 0) {
|
|
105
|
+
if (inputIndex === 0 && sinkContext.value === result.value) {
|
|
106
|
+
findings = {
|
|
107
|
+
startIndex: 0,
|
|
108
|
+
endIndex: result.value.length - 1,
|
|
109
|
+
overrunIndex: 0,
|
|
110
|
+
boundaryIndex: 0,
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
findings = agentLib.checkSqlInjectionSink(
|
|
114
|
+
inputIndex,
|
|
115
|
+
result.value.length,
|
|
116
|
+
2,
|
|
117
|
+
sinkContext.value,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
findings = {
|
|
109
|
-
startIndex: 0,
|
|
110
|
-
endIndex: result.value.length - 1,
|
|
111
|
-
overrunIndex: 0,
|
|
112
|
-
boundaryIndex: 0,
|
|
113
|
-
};
|
|
114
|
-
} else {
|
|
115
|
-
findings = agentLib.checkSqlInjectionSink(
|
|
116
|
-
inputIndex,
|
|
117
|
-
result.value.length,
|
|
118
|
-
2,
|
|
119
|
-
sinkContext.value,
|
|
120
|
-
);
|
|
121
|
+
inputIndex = sinkContext.value.indexOf(result.value, inputIndex + 1);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
if (findings) {
|
|
@@ -210,25 +211,27 @@ module.exports = function(core) {
|
|
|
210
211
|
for (const v of sinkValuesArr) {
|
|
211
212
|
if (findings) break;
|
|
212
213
|
|
|
213
|
-
|
|
214
|
+
let inputIndex = v.indexOf(result.value);
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
while (!findings && inputIndex >= 0) {
|
|
217
|
+
if (inputIndex === 0 && v === result.value) {
|
|
218
|
+
findings = {
|
|
219
|
+
startIndex: 0,
|
|
220
|
+
endIndex: result?.value.length - 1,
|
|
221
|
+
boundaryIndex: 0,
|
|
222
|
+
codeString: result.value
|
|
223
|
+
};
|
|
224
|
+
} else {
|
|
225
|
+
const endIndex = inputIndex + result?.value.length;
|
|
226
|
+
findings = agentLib.checkSsjsInjectionSink(v, inputIndex, result.value.length) && {
|
|
227
|
+
startIndex: inputIndex,
|
|
228
|
+
endIndex,
|
|
229
|
+
boundaryIndex: inputIndex,
|
|
230
|
+
codeString: result.value
|
|
231
|
+
};
|
|
232
|
+
}
|
|
223
233
|
|
|
224
|
-
|
|
225
|
-
const endIndex = inputIndex + result?.value.length;
|
|
226
|
-
findings = agentLib.checkSsjsInjectionSink(v, inputIndex, endIndex) && {
|
|
227
|
-
startIndex: inputIndex,
|
|
228
|
-
endIndex,
|
|
229
|
-
boundaryIndex: inputIndex,
|
|
230
|
-
codeString: result.value
|
|
231
|
-
};
|
|
234
|
+
inputIndex = v.indexOf(result.value, inputIndex + 1);
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -308,34 +311,30 @@ function handleStringValue(result, cmd, agentLib) {
|
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
let findings = null;
|
|
311
|
-
let inputIndex =
|
|
312
|
-
|
|
314
|
+
let inputIndex = cmd.indexOf(result.value);
|
|
315
|
+
|
|
316
|
+
while (!findings && inputIndex >= 0) {
|
|
317
|
+
if (inputIndex === 0 && cmd === result.value) {
|
|
318
|
+
findings = {
|
|
319
|
+
start: 0,
|
|
320
|
+
end: result.value.length - 1,
|
|
321
|
+
boundaryOverrunIndex: 0,
|
|
322
|
+
inputBoundaryIndex: 0,
|
|
323
|
+
};
|
|
324
|
+
} else {
|
|
325
|
+
const isAttack = agentLib.checkSsjsInjectionSink(cmd, inputIndex, result.value.length);
|
|
313
326
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
if (isAttack) {
|
|
328
|
+
findings = {
|
|
329
|
+
start: inputIndex,
|
|
330
|
+
end: inputIndex + result.value.length - 1,
|
|
331
|
+
boundaryOverrunIndex: 0,
|
|
332
|
+
inputBoundaryIndex: 0,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
318
336
|
|
|
319
|
-
|
|
320
|
-
findings = {
|
|
321
|
-
start: 0,
|
|
322
|
-
end: result.value.length - 1,
|
|
323
|
-
boundaryOverrunIndex: 0,
|
|
324
|
-
inputBoundaryIndex: 0,
|
|
325
|
-
};
|
|
326
|
-
} else {
|
|
327
|
-
// This is a temporary workaround, while `agent-lib` fixes
|
|
328
|
-
// the `checkSsjsInjectionSink` so it can detect the "TRUE-CLAUSE-1" correctly
|
|
329
|
-
// TODO: NODE-2897
|
|
330
|
-
const isAttack = result.idsList.includes('TRUE-CLAUSE-1') || agentLib.checkSsjsInjectionSink(cmd, inputIndex, result.value.length);
|
|
331
|
-
if (!isAttack) return findings;
|
|
332
|
-
|
|
333
|
-
findings = {
|
|
334
|
-
start: inputIndex,
|
|
335
|
-
end: inputIndex + result.value.length - 1,
|
|
336
|
-
boundaryOverrunIndex: 0,
|
|
337
|
-
inputBoundaryIndex: 0,
|
|
338
|
-
};
|
|
337
|
+
inputIndex = cmd.indexOf(result.value, inputIndex + 1);
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
return findings;
|
|
@@ -32,12 +32,17 @@ module.exports = function(core) {
|
|
|
32
32
|
if (isString(value)) {
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
if (isString(value.sql)) {
|
|
37
|
+
return value.sql;
|
|
38
|
+
}
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
mysqlInstr.install = function() {
|
|
38
42
|
[
|
|
39
43
|
{ module: 'mysql', file: 'lib/Connection.js', method: 'query' },
|
|
40
|
-
{ module: 'mysql2', file: 'lib/
|
|
44
|
+
{ module: 'mysql2', file: 'lib/connection.js', method: 'execute' },
|
|
45
|
+
{ module: 'mysql2', file: 'lib/connection.js', method: 'query' }
|
|
41
46
|
].forEach(
|
|
42
47
|
({ module, file, method }) => {
|
|
43
48
|
depHooks.resolve({ module, file, method }, conn => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
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)",
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/agent-lib": "^5.3.
|
|
21
|
-
"@contrast/common": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/esm-hooks": "1.
|
|
24
|
-
"@contrast/scopes": "1.
|
|
20
|
+
"@contrast/agent-lib": "^5.3.4",
|
|
21
|
+
"@contrast/common": "1.4.0",
|
|
22
|
+
"@contrast/core": "1.11.0",
|
|
23
|
+
"@contrast/esm-hooks": "1.7.0",
|
|
24
|
+
"@contrast/scopes": "1.3.0",
|
|
25
25
|
"ipaddr.js": "^2.0.1",
|
|
26
26
|
"semver": "^7.3.7"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"async-hook-domain": "^3.0.2"
|
|
27
30
|
}
|
|
28
31
|
}
|