@contrast/assess 1.25.0 → 1.26.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/dataflow/propagation/install/ejs/template.js +3 -2
- package/lib/dataflow/propagation/install/pug/index.js +4 -3
- package/lib/dataflow/propagation/install/string/split.js +38 -35
- package/lib/get-policy.js +120 -16
- package/lib/get-source-context.js +2 -1
- package/lib/make-source-context.js +1 -1
- package/package.json +1 -1
|
@@ -37,7 +37,8 @@ module.exports = function (core) {
|
|
|
37
37
|
rewriter,
|
|
38
38
|
} = core;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/** @type {import('@contrast/rewriter').RewriteOpts} */
|
|
41
|
+
const REWRITE_OPTS = { isModule: false, inject: false, wrap: false, trim: true };
|
|
41
42
|
const WRAPPER_PREFIX = join([
|
|
42
43
|
'function tempWrapper() {',
|
|
43
44
|
'function __append(s) { if (s !== undefined && s !== null) __output += s }'
|
|
@@ -63,7 +64,7 @@ module.exports = function (core) {
|
|
|
63
64
|
if (!getSourceContext(PROPAGATOR)) return;
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
|
-
const { code } = rewriter.
|
|
67
|
+
const { code } = rewriter.rewriteSync(`${WRAPPER_PREFIX}${data.obj.source}${WRAPPER_SUFFIX}`, REWRITE_OPTS);
|
|
67
68
|
data.obj.source = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
|
|
68
69
|
} catch (err) {
|
|
69
70
|
logger.error(
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
const { patchType } = require('../../common');
|
|
18
18
|
|
|
19
|
+
/** @type {import('@contrast/rewriter').RewriteOpts} */
|
|
20
|
+
const REWRITE_OPTS = { isModule: false, inject: false, wrap: false, trim: true };
|
|
21
|
+
|
|
19
22
|
module.exports = function (core) {
|
|
20
23
|
const store = { lock: true, name: 'assess:propagators:pug-compile' };
|
|
21
24
|
const {
|
|
@@ -23,8 +26,6 @@ module.exports = function (core) {
|
|
|
23
26
|
patcher, logger, rewriter, depHooks,
|
|
24
27
|
} = core;
|
|
25
28
|
|
|
26
|
-
const rewriterOpts = { isModule: false, wrap: false };
|
|
27
|
-
|
|
28
29
|
const pugInstrumentation = {
|
|
29
30
|
install() {
|
|
30
31
|
depHooks.resolve(
|
|
@@ -42,7 +43,7 @@ module.exports = function (core) {
|
|
|
42
43
|
postCodeGen: (value, options) => {
|
|
43
44
|
try {
|
|
44
45
|
return instrumentation.run(store,
|
|
45
|
-
() => rewriter.
|
|
46
|
+
() => rewriter.rewriteSync(value, REWRITE_OPTS).code
|
|
46
47
|
);
|
|
47
48
|
} catch (err) {
|
|
48
49
|
logger.warn({ err, funcKey: data.funcKey }, 'Failed to rewrite pug code');
|
|
@@ -25,7 +25,7 @@ module.exports = function(core) {
|
|
|
25
25
|
scopes: { sources, instrumentation },
|
|
26
26
|
patcher,
|
|
27
27
|
assess: {
|
|
28
|
-
eventFactory
|
|
28
|
+
eventFactory,
|
|
29
29
|
dataflow: { tracker }
|
|
30
30
|
}
|
|
31
31
|
} = core;
|
|
@@ -53,6 +53,40 @@ module.exports = function(core) {
|
|
|
53
53
|
const objInfo = tracker.getData(obj);
|
|
54
54
|
if (!objInfo || obj === result[0]) return;
|
|
55
55
|
|
|
56
|
+
const args = origArgs.map((arg) => {
|
|
57
|
+
const argInfo = tracker.getData(arg);
|
|
58
|
+
return {
|
|
59
|
+
value: argInfo ? argInfo.value : inspect(arg),
|
|
60
|
+
tracked: !!argInfo
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const event = eventFactory.createPropagationEvent({
|
|
65
|
+
name,
|
|
66
|
+
moduleName: 'String',
|
|
67
|
+
methodName: 'prototype.split',
|
|
68
|
+
context: `'${objInfo.value}'.split(${join(args.map(a => a.value), ', ')})`,
|
|
69
|
+
history: [objInfo],
|
|
70
|
+
object: {
|
|
71
|
+
value: obj,
|
|
72
|
+
tracked: true,
|
|
73
|
+
},
|
|
74
|
+
args,
|
|
75
|
+
tags: {},
|
|
76
|
+
result: {
|
|
77
|
+
value: join(result),
|
|
78
|
+
tracked: false
|
|
79
|
+
},
|
|
80
|
+
stacktraceOpts: {
|
|
81
|
+
constructorOpt: hooked,
|
|
82
|
+
prependFrames: [orig]
|
|
83
|
+
},
|
|
84
|
+
source: 'O',
|
|
85
|
+
target: 'R'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!event) return;
|
|
89
|
+
|
|
56
90
|
let idx = 0;
|
|
57
91
|
for (let i = 0; i < result.length; i++) {
|
|
58
92
|
const res = result[i];
|
|
@@ -64,40 +98,9 @@ module.exports = function(core) {
|
|
|
64
98
|
const tags = createSubsetTags(objInfo.tags, start, res.length);
|
|
65
99
|
if (!tags) continue;
|
|
66
100
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
value: argInfo ? argInfo.value : inspect(arg),
|
|
71
|
-
tracked: !!argInfo
|
|
72
|
-
};
|
|
73
|
-
});
|
|
74
|
-
const event = createPropagationEvent({
|
|
75
|
-
name,
|
|
76
|
-
moduleName: 'String',
|
|
77
|
-
methodName: 'prototype.split',
|
|
78
|
-
context: `'${objInfo.value}'.split(${join(args.map(a => a.value), ', ')})`,
|
|
79
|
-
history: [objInfo],
|
|
80
|
-
object: {
|
|
81
|
-
value: obj,
|
|
82
|
-
tracked: true,
|
|
83
|
-
},
|
|
84
|
-
args,
|
|
85
|
-
tags,
|
|
86
|
-
result: {
|
|
87
|
-
value: join(result),
|
|
88
|
-
tracked: false
|
|
89
|
-
},
|
|
90
|
-
stacktraceOpts: {
|
|
91
|
-
constructorOpt: hooked,
|
|
92
|
-
prependFrames: [orig]
|
|
93
|
-
},
|
|
94
|
-
source: 'O',
|
|
95
|
-
target: 'R'
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (!event) continue;
|
|
99
|
-
|
|
100
|
-
const { extern } = tracker.track(res, event);
|
|
101
|
+
const metadata = { ...event, tags };
|
|
102
|
+
eventFactory.createdEvents.add(metadata);
|
|
103
|
+
const { extern } = tracker.track(res, metadata);
|
|
101
104
|
|
|
102
105
|
if (extern) {
|
|
103
106
|
data.result[i] = extern;
|
package/lib/get-policy.js
CHANGED
|
@@ -20,6 +20,8 @@ const {
|
|
|
20
20
|
ResponseScanningRule,
|
|
21
21
|
SessionConfigurationRule,
|
|
22
22
|
Event,
|
|
23
|
+
join,
|
|
24
|
+
toLowerCase,
|
|
23
25
|
} = require('@contrast/common');
|
|
24
26
|
|
|
25
27
|
const rulesIds = Object.values({
|
|
@@ -28,6 +30,38 @@ const rulesIds = Object.values({
|
|
|
28
30
|
...SessionConfigurationRule,
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
function buildUriPathRegExp(urls) {
|
|
34
|
+
let regExpNeeded = false;
|
|
35
|
+
for (const url of urls) {
|
|
36
|
+
if (regExpCheck(url)) {
|
|
37
|
+
regExpNeeded = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (regExpNeeded) {
|
|
41
|
+
const rx = new RegExp(`^${join(urls, '|')}$`);
|
|
42
|
+
|
|
43
|
+
return (uriPath) => rx ? rx.test(uriPath) : false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (uriPath) => urls.some((url) => url === uriPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createUriPathMatcher(urls) {
|
|
50
|
+
if (urls.length) {
|
|
51
|
+
return buildUriPathRegExp(urls);
|
|
52
|
+
} else {
|
|
53
|
+
return () => true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function regExpCheck(str) {
|
|
58
|
+
return str.indexOf('*') > 0 ||
|
|
59
|
+
str.indexOf('.') > 0 ||
|
|
60
|
+
str.indexOf('+') > 0 ||
|
|
61
|
+
str.indexOf('?') > 0 ||
|
|
62
|
+
str.indexOf('\\') > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
/**
|
|
32
66
|
* @param {{
|
|
33
67
|
* config: import('@contrast/config').Config,
|
|
@@ -37,32 +71,102 @@ const rulesIds = Object.values({
|
|
|
37
71
|
* @returns {import('@contrast/common').Installable}
|
|
38
72
|
*/
|
|
39
73
|
module.exports = function assess(core) {
|
|
40
|
-
const { logger, messages } = core;
|
|
74
|
+
const { config, logger, messages } = core;
|
|
41
75
|
|
|
42
76
|
const enabledRules = new Set(rulesIds);
|
|
77
|
+
const exclusions = {
|
|
78
|
+
url: [],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function compileExclusions(settings) {
|
|
82
|
+
// reset global exclusion state
|
|
83
|
+
for (const key of Object.keys(exclusions)) {
|
|
84
|
+
exclusions[key] = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rawDtmList = [
|
|
88
|
+
// todo: NODE-3281 input exclusions
|
|
89
|
+
...(settings?.exclusions?.url || [])
|
|
90
|
+
].filter((exclusion) => exclusion.modes.includes('assess'));
|
|
91
|
+
|
|
92
|
+
if (!rawDtmList.length) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const dtm of rawDtmList) {
|
|
97
|
+
dtm.type = dtm.type || 'URL';
|
|
98
|
+
|
|
99
|
+
const { name, assess_rules, urls, type } = dtm;
|
|
100
|
+
const key = toLowerCase(type);
|
|
101
|
+
try {
|
|
102
|
+
const e = {
|
|
103
|
+
name,
|
|
104
|
+
rules: new Set(assess_rules),
|
|
105
|
+
};
|
|
106
|
+
e.matchesUriPath = createUriPathMatcher(urls);
|
|
107
|
+
exclusions[key].push(e);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger.error({ err, dtm }, 'failed to process exclusion');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
43
113
|
|
|
44
114
|
messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
45
|
-
if (!
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
115
|
+
if (!config.getEffectiveValue('assess.enable')) return;
|
|
116
|
+
|
|
117
|
+
if (msg.assess) {
|
|
118
|
+
for (const ruleId of rulesIds) {
|
|
119
|
+
const enable = msg.assess[ruleId]?.enable;
|
|
120
|
+
if (enable === true) {
|
|
121
|
+
enabledRules.add(ruleId);
|
|
122
|
+
if (ruleId === Rule.NOSQL_INJECTION) enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
|
|
123
|
+
} else if (enable === false) {
|
|
124
|
+
if (ruleId === Rule.NOSQL_INJECTION) enabledRules.delete(Rule.NOSQL_INJECTION_MONGO);
|
|
125
|
+
enabledRules.delete(ruleId);
|
|
126
|
+
}
|
|
55
127
|
}
|
|
56
128
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
129
|
+
|
|
130
|
+
if (msg.exclusions) {
|
|
131
|
+
compileExclusions(msg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
logger.info({ enabledRules: Array.from(enabledRules) }, 'Assess policy updated');
|
|
60
135
|
});
|
|
61
136
|
|
|
62
|
-
|
|
137
|
+
/**
|
|
138
|
+
* This gets called by assess.makeSourceContext(). We return copy of policy to avoid
|
|
139
|
+
* inconsistent behavior if policy is updated during request handling.
|
|
140
|
+
*/
|
|
141
|
+
return core.assess.getPolicy = function getPolicy({ uriPath } = {}) {
|
|
142
|
+
const _enabledRules = new Set(enabledRules);
|
|
143
|
+
|
|
144
|
+
// Evaluate url exclusions for current request.
|
|
145
|
+
// If one matches and applies to all rules, we return `null` for the policy value,
|
|
146
|
+
// which will disable assess for the request. If specific rules are disabled, we
|
|
147
|
+
// just removed them from the request policy's set of `enabledRules`.
|
|
148
|
+
for (const urlExclusion of exclusions.url) {
|
|
149
|
+
if (urlExclusion.matchesUriPath(uriPath)) {
|
|
150
|
+
if (!urlExclusion.rules?.size) {
|
|
151
|
+
core.logger.debug({
|
|
152
|
+
name: urlExclusion.name
|
|
153
|
+
}, 'all assess rules disabled by URL exclusion');
|
|
154
|
+
return null;
|
|
155
|
+
} else {
|
|
156
|
+
for (const ruleId of urlExclusion.rules) {
|
|
157
|
+
if (_enabledRules.has(ruleId)) _enabledRules.delete(ruleId);
|
|
158
|
+
}
|
|
159
|
+
core.logger.debug({
|
|
160
|
+
name: urlExclusion.name,
|
|
161
|
+
rules: Array.from(urlExclusion.rules),
|
|
162
|
+
}, 'assess rules disabled by URL exclusion');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
63
167
|
// creates copy of local policy for request store
|
|
64
168
|
return {
|
|
65
|
-
enabledRules: new Set(
|
|
169
|
+
enabledRules: new Set(_enabledRules),
|
|
66
170
|
};
|
|
67
171
|
};
|
|
68
172
|
};
|
|
@@ -38,7 +38,8 @@ module.exports = function(core) {
|
|
|
38
38
|
return core.assess.getSourceContext = function getSourceContext(type, ...rest) {
|
|
39
39
|
const ctx = sources.getStore()?.assess;
|
|
40
40
|
|
|
41
|
-
if
|
|
41
|
+
// policy will not exist if assess is altogether disabled for the active request e.g. url exclusion
|
|
42
|
+
if (!ctx || !ctx?.policy || instrumentation.isLocked()) return null;
|
|
42
43
|
|
|
43
44
|
switch (type) {
|
|
44
45
|
case InstrumentationType.PROPAGATOR: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.1",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Assess support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|