@contrast/protect 1.3.0 → 1.4.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/install/express4.js +2 -3
- package/lib/error-handlers/install/fastify3.js +2 -4
- 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 +4 -0
- package/lib/input-analysis/handlers.js +169 -7
- package/lib/input-analysis/index.js +4 -0
- package/lib/input-analysis/install/body-parser1.js +13 -21
- package/lib/input-analysis/install/cookie-parser1.js +13 -15
- package/lib/input-analysis/install/express4.js +8 -13
- package/lib/input-analysis/install/fastify3.js +4 -5
- package/lib/input-analysis/install/formidable1.js +4 -5
- package/lib/input-analysis/install/http.js +12 -3
- 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 +86 -22
- package/lib/input-tracing/index.js +10 -4
- 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/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 +4 -1
- package/lib/semantic-analysis/handlers.js +160 -0
- package/lib/semantic-analysis/index.js +38 -0
- package/package.json +7 -9
- package/lib/utils.js +0 -84
|
@@ -24,7 +24,6 @@ module.exports = function(core) {
|
|
|
24
24
|
logger,
|
|
25
25
|
depHooks,
|
|
26
26
|
patcher,
|
|
27
|
-
scopes: { sources },
|
|
28
27
|
protect,
|
|
29
28
|
} = core;
|
|
30
29
|
|
|
@@ -41,7 +40,7 @@ module.exports = function(core) {
|
|
|
41
40
|
patchType,
|
|
42
41
|
around(org, data) {
|
|
43
42
|
const [err] = data.args;
|
|
44
|
-
const sourceContext =
|
|
43
|
+
const sourceContext = protect.getSourceContext('finalHandler');
|
|
45
44
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
46
45
|
|
|
47
46
|
if (isSecurityException && sourceContext) {
|
|
@@ -67,7 +66,7 @@ module.exports = function(core) {
|
|
|
67
66
|
patchType,
|
|
68
67
|
around(org, data) {
|
|
69
68
|
const [err] = data.args;
|
|
70
|
-
const sourceContext =
|
|
69
|
+
const sourceContext = protect.getSourceContext('express.Layer.handle_error');
|
|
71
70
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
72
71
|
|
|
73
72
|
if (isSecurityException && sourceContext) {
|
|
@@ -20,10 +20,8 @@ const { patchType } = require('../constants');
|
|
|
20
20
|
|
|
21
21
|
module.exports = function(core) {
|
|
22
22
|
const {
|
|
23
|
-
logger,
|
|
24
23
|
depHooks,
|
|
25
24
|
patcher,
|
|
26
|
-
scopes: { sources },
|
|
27
25
|
protect,
|
|
28
26
|
} = core;
|
|
29
27
|
|
|
@@ -59,9 +57,9 @@ module.exports = function(core) {
|
|
|
59
57
|
const normalHandler = fastify3ErrorHandler._userHandler || fastify3ErrorHandler.defaultErrorHandler;
|
|
60
58
|
|
|
61
59
|
if (isSecurityException(err)) {
|
|
62
|
-
const sourceContext =
|
|
60
|
+
const sourceContext = protect.getSourceContext('fastify3.errorHandler');
|
|
61
|
+
|
|
63
62
|
if (!sourceContext) {
|
|
64
|
-
logger.info('source context not found; unable to handle response');
|
|
65
63
|
normalHandler.call(this, err, request, reply);
|
|
66
64
|
} else {
|
|
67
65
|
const blockInfo = sourceContext.findings.securityException;
|
|
@@ -24,7 +24,6 @@ module.exports = function(core) {
|
|
|
24
24
|
logger,
|
|
25
25
|
depHooks,
|
|
26
26
|
patcher,
|
|
27
|
-
scopes: { sources },
|
|
28
27
|
protect,
|
|
29
28
|
} = core;
|
|
30
29
|
|
|
@@ -42,7 +41,7 @@ module.exports = function(core) {
|
|
|
42
41
|
patchType,
|
|
43
42
|
around(org, data) {
|
|
44
43
|
const [err] = data.args;
|
|
45
|
-
const sourceContext =
|
|
44
|
+
const sourceContext = protect.getSourceContext('Koa.Application.handleRequest');
|
|
46
45
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
47
46
|
|
|
48
47
|
if (isSecurityException && sourceContext) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
/*
|
|
4
2
|
* Copyright: 2022 Contrast Security, Inc
|
|
5
3
|
* Contact: support@contrastsecurity.com
|
|
@@ -17,19 +15,19 @@
|
|
|
17
15
|
|
|
18
16
|
'use strict';
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
module.exports = function(core) {
|
|
19
|
+
const { scopes: { sources }, logger } = core;
|
|
20
|
+
|
|
21
|
+
function getSourceContext(callPoint) {
|
|
22
|
+
const sourceContext = sources.getStore()?.protect;
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const protect = require('./index')(deps);
|
|
26
|
-
protect.rewriting.install();
|
|
27
|
-
} catch (err) {
|
|
28
|
-
// TODO: something else
|
|
29
|
-
throw err;
|
|
24
|
+
if (!sourceContext) {
|
|
25
|
+
logger.debug(`source context not available in ${callPoint}`);
|
|
26
|
+
return null;
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
process.exit(1);
|
|
28
|
+
|
|
29
|
+
return sourceContext.allowed ? null : sourceContext;
|
|
34
30
|
}
|
|
35
|
-
|
|
31
|
+
|
|
32
|
+
core.protect.getSourceContext = getSourceContext;
|
|
33
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
module.exports = {
|
|
19
|
+
patchType: 'protect-hardening'
|
|
20
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
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 { BLOCKING_MODES, isString } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
const NODE_SERIALIZE_RCE_TOKEN = '_$$ND_FUNC$$_';
|
|
21
|
+
|
|
22
|
+
module.exports = function(core) {
|
|
23
|
+
const {
|
|
24
|
+
protect: {
|
|
25
|
+
hardening,
|
|
26
|
+
throwSecurityException,
|
|
27
|
+
}
|
|
28
|
+
} = core;
|
|
29
|
+
|
|
30
|
+
function getResults(sourceContext, ruleId) {
|
|
31
|
+
let results = sourceContext.findings.hardeningResultsMap[ruleId];
|
|
32
|
+
if (!results) {
|
|
33
|
+
results = sourceContext.findings.hardeningResultsMap[ruleId] = [];
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
|
|
39
|
+
const ruleId = 'untrusted-deserialization';
|
|
40
|
+
const { mode } = sourceContext.rules.agentRules[ruleId];
|
|
41
|
+
const { name, value } = sinkContext;
|
|
42
|
+
|
|
43
|
+
if (mode === 'off') return;
|
|
44
|
+
|
|
45
|
+
if (name === 'node-serialize.unserialize') {
|
|
46
|
+
if (!isString(value) || !value.indexOf(NODE_SERIALIZE_RCE_TOKEN)) return;
|
|
47
|
+
|
|
48
|
+
const blocked = BLOCKING_MODES.includes(mode);
|
|
49
|
+
const results = getResults(sourceContext, ruleId);
|
|
50
|
+
|
|
51
|
+
results.push({
|
|
52
|
+
blocked,
|
|
53
|
+
findings: { deserializer: name, command: false },
|
|
54
|
+
sinkContext,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (blocked) {
|
|
58
|
+
sourceContext.findings.securityException = [mode, ruleId];
|
|
59
|
+
throwSecurityException(sourceContext);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return hardening;
|
|
65
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
module.exports = function(core) {
|
|
19
|
+
const hardening = core.protect.hardening = {};
|
|
20
|
+
|
|
21
|
+
require('./handlers')(core);
|
|
22
|
+
|
|
23
|
+
require('./install/node-serialize0')(core);
|
|
24
|
+
hardening.install = function() {
|
|
25
|
+
hardening.nodeSerialize0Instrumentation.install();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return hardening;
|
|
29
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const {
|
|
22
|
+
depHooks,
|
|
23
|
+
patcher,
|
|
24
|
+
captureStacktrace,
|
|
25
|
+
protect,
|
|
26
|
+
protect: {
|
|
27
|
+
hardening
|
|
28
|
+
}
|
|
29
|
+
} = core;
|
|
30
|
+
|
|
31
|
+
function install() {
|
|
32
|
+
const name = 'node-serialize';
|
|
33
|
+
const method = 'unserialize';
|
|
34
|
+
|
|
35
|
+
depHooks.resolve(
|
|
36
|
+
{ name, version: '<1.0.0' },
|
|
37
|
+
(nodeSerialize) => {
|
|
38
|
+
patcher.patch(nodeSerialize, method, {
|
|
39
|
+
name,
|
|
40
|
+
patchType,
|
|
41
|
+
pre({ args: [value], hooked, orig }) {
|
|
42
|
+
const sourceContext = protect.getSourceContext(`${name}.${method}`);
|
|
43
|
+
|
|
44
|
+
if (!sourceContext || !value) return;
|
|
45
|
+
|
|
46
|
+
const sinkContext = captureStacktrace(
|
|
47
|
+
{ name: `${name}.${method}`, value },
|
|
48
|
+
{ constructorOpt: hooked, prependFrames: [orig] },
|
|
49
|
+
);
|
|
50
|
+
hardening.handleUntrustedDeserialization(sourceContext, sinkContext);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return hardening.nodeSerialize0Instrumentation = {
|
|
57
|
+
install
|
|
58
|
+
};
|
|
59
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -18,10 +18,9 @@ import { Core } from '@contrast/core';
|
|
|
18
18
|
import { Logger } from '@contrast/logger';
|
|
19
19
|
import { Sources } from '@contrast/scopes';
|
|
20
20
|
import RequireHook from '@contrast/require-hook';
|
|
21
|
-
import { RulesConfig,
|
|
21
|
+
import { RulesConfig, Messages, ReqData, ProtectMessage, Findings } from '@contrast/common';
|
|
22
22
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
23
23
|
import { Config } from '@contrast/config';
|
|
24
|
-
import { ProtectMessage } from '@contrast/common';
|
|
25
24
|
import * as http from 'node:http';
|
|
26
25
|
import * as https from 'node:https';
|
|
27
26
|
|
|
@@ -50,24 +49,6 @@ export class HttpInstrumentation {
|
|
|
50
49
|
initiateRequestHandling(fnContext: { instance: any, method: any, args: any }): void; //TODO
|
|
51
50
|
removeCookies(headers: string[]): string[];
|
|
52
51
|
}
|
|
53
|
-
export interface ReqData {
|
|
54
|
-
method: string;
|
|
55
|
-
headers: string[];
|
|
56
|
-
uriPath: string;
|
|
57
|
-
queries: string;
|
|
58
|
-
contentType?: string;
|
|
59
|
-
standardUrlParsing: boolean;
|
|
60
|
-
ip: string;
|
|
61
|
-
httpVersion: string,
|
|
62
|
-
headers2: { [key: string]: Array<string> };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface Findings {
|
|
66
|
-
trackRequest: boolean;
|
|
67
|
-
securityException?: [mode: string, ruleId: string];
|
|
68
|
-
bodyType?: 'json' | 'urlencoded';
|
|
69
|
-
resultsMap: Record<Rule, Result[]>
|
|
70
|
-
}
|
|
71
52
|
|
|
72
53
|
export interface ProtectRequestStore {
|
|
73
54
|
reqData: ReqData;
|
|
@@ -140,7 +121,8 @@ export interface Protect {
|
|
|
140
121
|
sequelizeInstrumentation: {
|
|
141
122
|
getQueryFromArgs: ([value]: any[]) => string | undefined,
|
|
142
123
|
install: () => void
|
|
143
|
-
}
|
|
124
|
+
},
|
|
125
|
+
httpInstrumentation: { install: () => void },
|
|
144
126
|
install: () => void
|
|
145
127
|
}
|
|
146
128
|
errorHandlers: {
|
package/lib/index.js
CHANGED
|
@@ -33,8 +33,11 @@ module.exports = function(core) {
|
|
|
33
33
|
require('./throw-security-exception')(core);
|
|
34
34
|
require('./make-response-blocker')(core);
|
|
35
35
|
require('./make-source-context')(core);
|
|
36
|
+
require('./get-source-context')(core);
|
|
36
37
|
require('./input-analysis')(core);
|
|
37
38
|
require('./input-tracing')(core);
|
|
39
|
+
require('./hardening')(core);
|
|
40
|
+
require('./semantic-analysis')(core);
|
|
38
41
|
require('./error-handlers')(core);
|
|
39
42
|
|
|
40
43
|
const pkj = require('../package.json');
|
|
@@ -43,6 +46,7 @@ module.exports = function(core) {
|
|
|
43
46
|
protect.install = function() {
|
|
44
47
|
protect.inputAnalysis.install();
|
|
45
48
|
protect.inputTracing.install();
|
|
49
|
+
protect.hardening.install();
|
|
46
50
|
protect.errorHandlers.install();
|
|
47
51
|
};
|
|
48
52
|
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { simpleTraverse } = require('
|
|
18
|
+
const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
|
|
19
|
+
const address = require('ipaddr.js');
|
|
19
20
|
|
|
20
21
|
//
|
|
21
22
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
@@ -110,8 +111,12 @@ module.exports = function(core) {
|
|
|
110
111
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
111
112
|
*/
|
|
112
113
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
114
|
+
if (!sourceContext || sourceContext.allowed) return;
|
|
115
|
+
|
|
113
116
|
const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
|
|
114
117
|
|
|
118
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
|
|
119
|
+
|
|
115
120
|
// initialize findings to the basics
|
|
116
121
|
let block = undefined;
|
|
117
122
|
if (mask !== 0) {
|
|
@@ -130,12 +135,12 @@ module.exports = function(core) {
|
|
|
130
135
|
throw new Error('nyi', sourceContext);
|
|
131
136
|
};
|
|
132
137
|
|
|
133
|
-
|
|
134
138
|
const jsonInputTypes = {
|
|
135
|
-
keyType: agentLib.InputType.JsonKey,
|
|
139
|
+
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
136
140
|
};
|
|
141
|
+
|
|
137
142
|
const parameterInputTypes = {
|
|
138
|
-
keyType: agentLib.InputType.ParameterKey,
|
|
143
|
+
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
139
144
|
};
|
|
140
145
|
|
|
141
146
|
/**
|
|
@@ -155,10 +160,17 @@ module.exports = function(core) {
|
|
|
155
160
|
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
156
161
|
*/
|
|
157
162
|
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
163
|
+
if (sourceContext.analyzedQuery) return;
|
|
164
|
+
sourceContext.analyzedQuery = true;
|
|
165
|
+
|
|
158
166
|
if (typeof queryParams !== 'object') {
|
|
159
167
|
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
173
|
+
|
|
162
174
|
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
163
175
|
|
|
164
176
|
if (block) {
|
|
@@ -176,11 +188,16 @@ module.exports = function(core) {
|
|
|
176
188
|
* @param {Object} urlParams pojo
|
|
177
189
|
*/
|
|
178
190
|
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
191
|
+
if (sourceContext.analyzedUrlParams) return;
|
|
192
|
+
sourceContext.analyzedUrlParams = true;
|
|
193
|
+
|
|
179
194
|
if (typeof urlParams !== 'object') {
|
|
180
195
|
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
181
196
|
return;
|
|
182
197
|
}
|
|
183
198
|
|
|
199
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
200
|
+
|
|
184
201
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
185
202
|
const resultsList = [];
|
|
186
203
|
const { UrlParameter } = agentLib.InputType;
|
|
@@ -235,10 +252,16 @@ module.exports = function(core) {
|
|
|
235
252
|
* @param {Object} cookies pojo
|
|
236
253
|
*/
|
|
237
254
|
inputAnalysis.handleCookies = function(sourceContext, cookies) {
|
|
255
|
+
if (sourceContext.analyzedCookies) return;
|
|
256
|
+
sourceContext.analyzedCookies = true;
|
|
257
|
+
|
|
238
258
|
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
239
259
|
acc.push(key, value);
|
|
240
260
|
return acc;
|
|
241
261
|
}, []);
|
|
262
|
+
|
|
263
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
264
|
+
|
|
242
265
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
243
266
|
const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
|
|
244
267
|
|
|
@@ -259,11 +282,16 @@ module.exports = function(core) {
|
|
|
259
282
|
* @param {Object} parsedBody
|
|
260
283
|
*/
|
|
261
284
|
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
285
|
+
if (sourceContext.analyzedBody) return;
|
|
286
|
+
sourceContext.analyzedBody = true;
|
|
287
|
+
|
|
262
288
|
if (typeof parsedBody !== 'object') {
|
|
263
289
|
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
264
290
|
return;
|
|
265
291
|
}
|
|
266
292
|
|
|
293
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
|
|
294
|
+
|
|
267
295
|
let bodyType;
|
|
268
296
|
let inputTypes;
|
|
269
297
|
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
@@ -288,6 +316,70 @@ module.exports = function(core) {
|
|
|
288
316
|
throw new Error('nyi', sourceContext, name);
|
|
289
317
|
};
|
|
290
318
|
|
|
319
|
+
inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
|
|
320
|
+
const ruleId = 'virtual-patch';
|
|
321
|
+
|
|
322
|
+
if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
|
|
323
|
+
|
|
324
|
+
for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
|
|
325
|
+
for (const key in requestInput) {
|
|
326
|
+
const evaluator = vpEvaluators.get(key);
|
|
327
|
+
|
|
328
|
+
if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
|
|
329
|
+
vpEvaluators.delete(key);
|
|
330
|
+
const { name, uuid } = vpEvaluators.get('metadata');
|
|
331
|
+
|
|
332
|
+
if (vpEvaluators.size === 1 && uuid) {
|
|
333
|
+
if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
|
|
334
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
|
|
335
|
+
}
|
|
336
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
|
|
337
|
+
name,
|
|
338
|
+
uuid
|
|
339
|
+
});
|
|
340
|
+
sourceContext.findings.securityException = ['block', ruleId];
|
|
341
|
+
core.protect.throwSecurityException(sourceContext);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
inputAnalysis.handleIpAllowlist = function(sourceContext, ipAllowlist) {
|
|
349
|
+
if (!sourceContext || !ipAllowlist.length) return;
|
|
350
|
+
|
|
351
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
352
|
+
|
|
353
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
|
|
354
|
+
|
|
355
|
+
if (match) {
|
|
356
|
+
logger.info(match, 'Found a matching IP to an entry in ipAllow list');
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
|
|
362
|
+
const ruleId = 'ip-denylist';
|
|
363
|
+
|
|
364
|
+
if (!sourceContext || !ipDenylist.length) return;
|
|
365
|
+
|
|
366
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
367
|
+
|
|
368
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
|
|
369
|
+
|
|
370
|
+
if (match) {
|
|
371
|
+
logger.info(match, 'Found a matching IP to an entry in ipDeny list');
|
|
372
|
+
if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
|
|
373
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
|
|
377
|
+
ip: match.matchedIp,
|
|
378
|
+
uuid: match.uuid,
|
|
379
|
+
});
|
|
380
|
+
return ['block', 'ip-denylist'];
|
|
381
|
+
}
|
|
382
|
+
};
|
|
291
383
|
|
|
292
384
|
/**
|
|
293
385
|
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
@@ -307,7 +399,7 @@ module.exports = function(core) {
|
|
|
307
399
|
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
308
400
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
309
401
|
// use inputTypes to set params...
|
|
310
|
-
const { keyType,
|
|
402
|
+
const { keyType, inputType } = inputTypes;
|
|
311
403
|
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
312
404
|
const { Where } = agentLib.MongoQueryType;
|
|
313
405
|
const resultsList = [];
|
|
@@ -340,7 +432,7 @@ module.exports = function(core) {
|
|
|
340
432
|
mongoQueryType = agentLib.getMongoQueryType(value);
|
|
341
433
|
}
|
|
342
434
|
} else {
|
|
343
|
-
itemType =
|
|
435
|
+
itemType = inputType;
|
|
344
436
|
}
|
|
345
437
|
let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
|
|
346
438
|
if (!items && !mongoQueryType) {
|
|
@@ -398,6 +490,50 @@ module.exports = function(core) {
|
|
|
398
490
|
|
|
399
491
|
return mergeFindings(rules.agentLibRules, sourceContext.findings, findings);
|
|
400
492
|
}
|
|
493
|
+
|
|
494
|
+
function ipListAnalysis(reqIp, reqHeaders, list) {
|
|
495
|
+
const forwardedIps = [];
|
|
496
|
+
|
|
497
|
+
for (let i = 0; i < reqHeaders.length; i++) {
|
|
498
|
+
if (reqHeaders[i] === 'x-forwarded-for') {
|
|
499
|
+
const ipsFromHeaders = reqHeaders[i + 1]?.split(/[,;]+/);
|
|
500
|
+
forwardedIps.push(...ipsFromHeaders);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const ipsToCheck = [reqIp, ...forwardedIps];
|
|
505
|
+
const now = new Date().getTime();
|
|
506
|
+
|
|
507
|
+
/* c8 ignore next 3 */
|
|
508
|
+
if (!ipsToCheck.length) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const listEntry of list) {
|
|
513
|
+
const { doesExpire, expiresAt } = listEntry;
|
|
514
|
+
for (let i = 0; i < ipsToCheck.length; i++) {
|
|
515
|
+
const currentIp = ipsToCheck[i];
|
|
516
|
+
|
|
517
|
+
// Ignore bad IP values.
|
|
518
|
+
if (!address.isValid(currentIp)) {
|
|
519
|
+
logger.warn(`Unable to parse ${currentIp}.`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const expired = doesExpire ? expiresAt - now <= 0 : false;
|
|
523
|
+
|
|
524
|
+
if (expired) {
|
|
525
|
+
logger.info(`IP expired: ${listEntry.name}, ${listEntry.ip}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const match = checkIpsMatch(listEntry, currentIp);
|
|
530
|
+
|
|
531
|
+
if (match) {
|
|
532
|
+
return match;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
401
537
|
};
|
|
402
538
|
|
|
403
539
|
/**
|
|
@@ -455,13 +591,38 @@ function normalizeFindings(rules, findings) {
|
|
|
455
591
|
// which the block can occur. so at a minimum 'block' should also result in a
|
|
456
592
|
// block.
|
|
457
593
|
const { mode } = rules[r.ruleId];
|
|
458
|
-
if (r.score >= 90 &&
|
|
594
|
+
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
|
|
459
595
|
r.blocked = true;
|
|
460
596
|
findings.securityException = [mode, r.ruleId];
|
|
461
597
|
}
|
|
462
598
|
}
|
|
463
599
|
}
|
|
464
600
|
|
|
601
|
+
|
|
602
|
+
function checkIpsMatch(listEntry, ip) {
|
|
603
|
+
const parsed = address.process(ip);
|
|
604
|
+
|
|
605
|
+
// Check if IP is in CIDR range,
|
|
606
|
+
if (listEntry.cidr) {
|
|
607
|
+
if (parsed.kind() !== listEntry.cidr.kind) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (parsed.match(listEntry.cidr.range)) {
|
|
612
|
+
return { ...listEntry, match: ip };
|
|
613
|
+
} else {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// or do a direct comparison
|
|
619
|
+
if (parsed.toNormalizedString() === listEntry.normalizedValue) {
|
|
620
|
+
return { ...listEntry, matchedIp: ip };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
465
626
|
/**
|
|
466
627
|
* getValueAtKey() is used to fetch the object (expected) associated
|
|
467
628
|
* with the path of keys in obj. i say expected because this is only used
|
|
@@ -476,6 +637,7 @@ function normalizeFindings(rules, findings) {
|
|
|
476
637
|
*/
|
|
477
638
|
function getValueAtKey(obj, path, key) {
|
|
478
639
|
for (const p of path) {
|
|
640
|
+
/* c8 ignore next 6 */
|
|
479
641
|
if (!(p in obj)) {
|
|
480
642
|
return undefined;
|
|
481
643
|
}
|
|
@@ -39,6 +39,10 @@ module.exports = function(core) {
|
|
|
39
39
|
require('./install/koa2')(core);
|
|
40
40
|
require('./install/express4')(core);
|
|
41
41
|
|
|
42
|
+
// virtual patches
|
|
43
|
+
require('./virtual-patches')(core);
|
|
44
|
+
require('./ip-analysis')(core);
|
|
45
|
+
|
|
42
46
|
inputAnalysis.install = function() {
|
|
43
47
|
Object.values(inputAnalysis)
|
|
44
48
|
.filter((property) => property.install)
|