@contrast/agent 4.8.0 → 4.10.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/bin/VERSION +1 -1
- package/bin/linux/contrast-service +0 -0
- package/bin/mac/contrast-service +0 -0
- package/bin/windows/contrast-service.exe +0 -0
- package/bootstrap.js +12 -2
- package/esm.mjs +33 -0
- package/lib/assess/index.js +2 -0
- package/lib/assess/models/source-event.js +6 -0
- package/lib/assess/policy/rules.json +29 -0
- package/lib/assess/policy/signatures.json +6 -0
- package/lib/assess/propagators/JSON/stringify.js +77 -7
- package/lib/assess/propagators/joi/any.js +48 -0
- package/lib/assess/propagators/joi/index.js +2 -0
- package/lib/assess/propagators/joi/object.js +61 -0
- package/lib/assess/propagators/joi/string-base.js +16 -0
- package/lib/assess/propagators/mongoose/helpers.js +36 -1
- package/lib/assess/propagators/mongoose/index.js +1 -0
- package/lib/assess/propagators/mongoose/map.js +19 -31
- package/lib/assess/propagators/mongoose/mixed.js +71 -0
- package/lib/assess/propagators/mongoose/string.js +8 -0
- package/lib/assess/sinks/rethinkdb-nosql-injection.js +142 -0
- package/lib/assess/sources/event-handler.js +307 -0
- package/lib/assess/sources/index.js +93 -5
- package/lib/assess/spdy/index.js +23 -0
- package/lib/assess/spdy/sinks/index.js +23 -0
- package/lib/assess/spdy/sinks/xss.js +84 -0
- package/lib/assess/technologies/index.js +2 -1
- package/lib/constants.js +2 -1
- package/lib/contrast.js +6 -6
- package/lib/core/arch-components/index.js +1 -0
- package/lib/core/arch-components/mongodb.js +22 -18
- package/lib/core/arch-components/mysql.js +25 -15
- package/lib/core/arch-components/postgres.js +40 -12
- package/lib/core/arch-components/sqlite3.js +3 -5
- package/lib/core/arch-components/util.js +49 -0
- package/lib/core/config/options.js +37 -1
- package/lib/core/exclusions/exclusion.js +2 -5
- package/lib/core/express/index.js +28 -2
- package/lib/core/express/utils.js +8 -3
- package/lib/core/fastify/index.js +2 -1
- package/lib/core/hapi/index.js +2 -1
- package/lib/core/koa/index.js +9 -1
- package/lib/core/rewrite/callees.js +16 -0
- package/lib/core/rewrite/import-declaration.js +71 -0
- package/lib/core/rewrite/index.js +9 -7
- package/lib/core/rewrite/injections.js +5 -1
- package/lib/hooks/frameworks/index.js +2 -0
- package/lib/hooks/frameworks/spdy.js +87 -0
- package/lib/hooks/http.js +11 -0
- package/lib/protect/restify/sources.js +35 -0
- package/lib/protect/rules/nosqli/nosql-injection-rule.js +30 -16
- package/lib/protect/rules/nosqli/nosql-scanner/index.js +1 -1
- package/lib/protect/rules/nosqli/nosql-scanner/rethinkdbscanner.js +26 -0
- package/lib/protect/sinks/index.js +2 -0
- package/lib/protect/sinks/mongodb.js +1 -3
- package/lib/protect/sinks/rethinkdb.js +47 -0
- package/lib/reporter/translations/to-protobuf/dtm/trace-event/index.js +4 -4
- package/lib/util/source-map.js +3 -3
- package/package.json +18 -12
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const tracker = require('../../../tracker');
|
|
18
|
+
const patcher = require('../../../hooks/patcher');
|
|
19
|
+
const requireHook = require('../../../hooks/require');
|
|
20
|
+
const tagRangeUtil = require('../../models/tag-range/util');
|
|
21
|
+
const {
|
|
22
|
+
PATCH_TYPES: { ASSESS_PROPAGATOR }
|
|
23
|
+
} = require('../../../constants');
|
|
24
|
+
const {
|
|
25
|
+
hasUserDefinedValidator,
|
|
26
|
+
tagCustomValidatedValues
|
|
27
|
+
} = require('./helpers');
|
|
28
|
+
const agent = require('../../../agent');
|
|
29
|
+
|
|
30
|
+
const doValidateSyncPatcher = (SchemaMap) => {
|
|
31
|
+
patcher.patch(SchemaMap.prototype, 'doValidateSync', {
|
|
32
|
+
alwaysRun: true,
|
|
33
|
+
name: 'mongoose.mixed.doValidateSync',
|
|
34
|
+
patchType: ASSESS_PROPAGATOR,
|
|
35
|
+
post(data) {
|
|
36
|
+
if (data.result || !hasUserDefinedValidator(data)) return;
|
|
37
|
+
|
|
38
|
+
const input = data.args[0];
|
|
39
|
+
const inputType = typeof input;
|
|
40
|
+
|
|
41
|
+
if (inputType !== 'string' && inputType !== 'object') return;
|
|
42
|
+
|
|
43
|
+
let values;
|
|
44
|
+
if (inputType === 'string') {
|
|
45
|
+
values = [input];
|
|
46
|
+
} else if (Array.isArray(input)) {
|
|
47
|
+
values = input;
|
|
48
|
+
} else if (input instanceof Map) {
|
|
49
|
+
values = input.values();
|
|
50
|
+
} else {
|
|
51
|
+
values = Object.values(input);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tagCustomValidatedValues(values, data, tracker, tagRangeUtil);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
requireHook.resolve(
|
|
60
|
+
{ name: 'mongoose', file: 'lib/schema/mixed.js', version: '>=5.0.0' },
|
|
61
|
+
(SchemaMap) => {
|
|
62
|
+
if (
|
|
63
|
+
!agent.config ||
|
|
64
|
+
(agent.config && !agent.config.agent.trust_custom_validators)
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
doValidateSyncPatcher(SchemaMap);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
const TagRange = require('../../models/tag-range');
|
|
25
25
|
const { CallContext, PropagationEvent, Signature } = require('../../models');
|
|
26
26
|
const { hasUserDefinedValidator } = require('./helpers');
|
|
27
|
+
const agent = require('../../../agent');
|
|
27
28
|
|
|
28
29
|
const enumPatcher = (SchemaString) => {
|
|
29
30
|
patcher.patch(SchemaString.prototype, 'enum', {
|
|
@@ -98,6 +99,13 @@ const doValidateSyncPatcher = (SchemaString) => {
|
|
|
98
99
|
requireHook.resolve(
|
|
99
100
|
{ name: 'mongoose', file: 'lib/schema/string.js', version: '>=5.0.0' },
|
|
100
101
|
(SchemaString) => {
|
|
102
|
+
if (
|
|
103
|
+
!agent.config ||
|
|
104
|
+
(agent.config && !agent.config.agent.trust_custom_validators)
|
|
105
|
+
) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
enumPatcher(SchemaString);
|
|
102
110
|
doValidateSyncPatcher(SchemaString);
|
|
103
111
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const patcher = require('../../hooks/patcher');
|
|
18
|
+
const { PATCH_TYPES } = require('../../constants');
|
|
19
|
+
const moduleHook = require('../../hooks/require');
|
|
20
|
+
const { CallContext, Signature } = require('../models');
|
|
21
|
+
const {
|
|
22
|
+
RULES: { NOSQL_INJECTION }
|
|
23
|
+
} = require('../../constants');
|
|
24
|
+
|
|
25
|
+
const filterSignature = new Signature({
|
|
26
|
+
moduleName: 'rethinkdb.RDBVal',
|
|
27
|
+
methodName: 'filter',
|
|
28
|
+
isModule: true
|
|
29
|
+
});
|
|
30
|
+
const matchSignature = new Signature({
|
|
31
|
+
moduleName: 'rethinkdb.RDBVal',
|
|
32
|
+
methodName: 'match',
|
|
33
|
+
isModule: true
|
|
34
|
+
});
|
|
35
|
+
const moduleName = 'rethinkdb';
|
|
36
|
+
|
|
37
|
+
class Handler {
|
|
38
|
+
constructor({ report, isVulnerable, requiredTags }) {
|
|
39
|
+
this._isVulnerable = isVulnerable;
|
|
40
|
+
this.report = report;
|
|
41
|
+
this.requiredTags = requiredTags;
|
|
42
|
+
this.disallowedTags = [
|
|
43
|
+
'alphanum-space-hyphen',
|
|
44
|
+
'limited-chars',
|
|
45
|
+
'custom-validated',
|
|
46
|
+
'custom-encoded-nosql-injection',
|
|
47
|
+
'custom-validated-nosql-injection'
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
isVulnerable(input) {
|
|
52
|
+
const { requiredTags, disallowedTags } = this;
|
|
53
|
+
return this._isVulnerable({
|
|
54
|
+
input,
|
|
55
|
+
ruleId: NOSQL_INJECTION,
|
|
56
|
+
disallowedTags,
|
|
57
|
+
requiredTags,
|
|
58
|
+
searchDepth: 5
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handle() {
|
|
63
|
+
moduleHook.resolve({ name: moduleName }, (rethinkdb) =>
|
|
64
|
+
this.handleRequire(rethinkdb)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
patchRethinkDb(rethinkdb) {
|
|
69
|
+
const self = this;
|
|
70
|
+
patcher.patch(rethinkdb, 'table', {
|
|
71
|
+
name: moduleName,
|
|
72
|
+
patchType: PATCH_TYPES.ASSESS_SINK,
|
|
73
|
+
alwaysRun: true,
|
|
74
|
+
post(data) {
|
|
75
|
+
self.patchTableMethod(data.result);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Patch the table method and through it gain access to the object
|
|
82
|
+
* prototype that holds the vulnerable `match` and `filter` methods
|
|
83
|
+
*/
|
|
84
|
+
patchTableMethod(tableObject) {
|
|
85
|
+
const RDBValObject = Object.getPrototypeOf(tableObject.args[0]);
|
|
86
|
+
const TermBaseObject = Object.getPrototypeOf(RDBValObject);
|
|
87
|
+
const self = this;
|
|
88
|
+
|
|
89
|
+
patcher.patch(TermBaseObject, 'match', {
|
|
90
|
+
name: 'TermBase.prototype',
|
|
91
|
+
patchType: PATCH_TYPES.ASSESS_SINK,
|
|
92
|
+
alwaysRun: true,
|
|
93
|
+
post({ args: [str], hooked, obj }) {
|
|
94
|
+
if (self.isVulnerable(str)) {
|
|
95
|
+
self.report({
|
|
96
|
+
ruleId: NOSQL_INJECTION,
|
|
97
|
+
signature: matchSignature,
|
|
98
|
+
input: str,
|
|
99
|
+
ctxt: new CallContext({
|
|
100
|
+
obj,
|
|
101
|
+
args: [str],
|
|
102
|
+
result: str,
|
|
103
|
+
stackOpts: {
|
|
104
|
+
constructorOpt: hooked
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
patcher.patch(TermBaseObject, 'filter', {
|
|
112
|
+
name: 'TermBase.prototype',
|
|
113
|
+
patchType: PATCH_TYPES.ASSESS_SINK,
|
|
114
|
+
alwaysRun: true,
|
|
115
|
+
post({ args: [str], hooked, obj }) {
|
|
116
|
+
if (self.isVulnerable(str)) {
|
|
117
|
+
self.report({
|
|
118
|
+
ruleId: NOSQL_INJECTION,
|
|
119
|
+
signature: filterSignature,
|
|
120
|
+
input: str,
|
|
121
|
+
ctxt: new CallContext({
|
|
122
|
+
obj,
|
|
123
|
+
args: [str],
|
|
124
|
+
result: str,
|
|
125
|
+
stackOpts: {
|
|
126
|
+
constructorOpt: hooked
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
handleRequire(rethinkdb) {
|
|
136
|
+
this.patchRethinkDb(rethinkdb);
|
|
137
|
+
return rethinkdb;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = ({ common }) => new Handler(common);
|
|
142
|
+
module.exports.Handler = Handler;
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const logger = require('../../core/logger')('contrast:assess.sources');
|
|
18
|
+
const { PATCH_TYPES } = require('../../constants');
|
|
19
|
+
const { CallContext, SourceEvent } = require('../models');
|
|
20
|
+
const TraceEventSource = require('../../reporter/models/trace-event-source');
|
|
21
|
+
const { AsyncStorage, KEYS } = require('../../core/async-storage');
|
|
22
|
+
const TagRange = require('../models/tag-range');
|
|
23
|
+
const patcher = require('../../hooks/patcher');
|
|
24
|
+
const tracker = require('../../tracker');
|
|
25
|
+
const parseurl = require('parseurl');
|
|
26
|
+
|
|
27
|
+
const SOURCE_EVENT_MAX = 250;
|
|
28
|
+
const trackedObjects = new WeakMap();
|
|
29
|
+
|
|
30
|
+
class SourceEventHandler {
|
|
31
|
+
constructor({
|
|
32
|
+
config = {
|
|
33
|
+
agent: {
|
|
34
|
+
node: {
|
|
35
|
+
array_request_sampling: {
|
|
36
|
+
enable: true,
|
|
37
|
+
threshold: 50,
|
|
38
|
+
interval: 5
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
ensureReq = true,
|
|
44
|
+
exclusions,
|
|
45
|
+
stackOpts,
|
|
46
|
+
snapshot,
|
|
47
|
+
signature,
|
|
48
|
+
type = 'UNKNOWN'
|
|
49
|
+
} = {}) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.exclusions = exclusions;
|
|
52
|
+
this.type = type;
|
|
53
|
+
this.snapshot = snapshot;
|
|
54
|
+
this.signature = signature;
|
|
55
|
+
this.stackOpts = stackOpts;
|
|
56
|
+
this.ensureReq = ensureReq;
|
|
57
|
+
this.samplingEnabled = config.agent.node.array_request_sampling.enable;
|
|
58
|
+
this.samplingThreshold = config.agent.node.array_request_sampling.threshold;
|
|
59
|
+
this.samplingInterval = this.samplingEnabled
|
|
60
|
+
? config.agent.node.array_request_sampling.interval
|
|
61
|
+
: 1;
|
|
62
|
+
|
|
63
|
+
this.inputExclusions = [];
|
|
64
|
+
this.urlExclusions = [];
|
|
65
|
+
this.skipEvent = false;
|
|
66
|
+
this.sourceEventCount = 0;
|
|
67
|
+
|
|
68
|
+
this.init();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
init() {
|
|
72
|
+
// values reused in functions
|
|
73
|
+
this.req = AsyncStorage.get(KEYS.REQ);
|
|
74
|
+
|
|
75
|
+
// NODE-1431: prevents crashing when req is not present in AsyncStorage.
|
|
76
|
+
if (!this.req) {
|
|
77
|
+
if (!this.ensureReq) return;
|
|
78
|
+
|
|
79
|
+
this.skipEvent = true;
|
|
80
|
+
logger.debug(
|
|
81
|
+
'failed to handle source event for type: %s; request not present in async storage',
|
|
82
|
+
this.type
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.exclusions) return;
|
|
87
|
+
|
|
88
|
+
const { pathname } = parseurl(this.req);
|
|
89
|
+
|
|
90
|
+
this.inputExclusions = this.exclusions
|
|
91
|
+
.getInputExclusions('assess')
|
|
92
|
+
.reduce((acc, e) => {
|
|
93
|
+
if (!e.matchesUrl(pathname)) return acc;
|
|
94
|
+
|
|
95
|
+
// `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
|
|
96
|
+
// apply to the entire set of params not only those by a specified name. Also
|
|
97
|
+
// is true if the input name is set to wildcard "*" meaning it should apply to
|
|
98
|
+
// all values. In each case there's no need to wrap if all rules are excluded.
|
|
99
|
+
if (!this.skipEvent && e.appliesToAllAssessRules() && !e.isNamed) {
|
|
100
|
+
logExclusionMessage(e, this.type);
|
|
101
|
+
this.skipEvent = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
acc.push(e);
|
|
105
|
+
return acc;
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
this.urlExclusions = this.exclusions
|
|
109
|
+
.getUrlExclusions('assess')
|
|
110
|
+
.reduce((acc, e) => {
|
|
111
|
+
if (!e.matchesUrl(pathname)) return acc;
|
|
112
|
+
|
|
113
|
+
if (!this.skipEvent && e.appliesToAllAssessRules()) {
|
|
114
|
+
logExclusionMessage(e, this.type);
|
|
115
|
+
this.skipEvent = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
acc.push(e);
|
|
119
|
+
return acc;
|
|
120
|
+
}, []);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
*
|
|
125
|
+
* @param {object} param
|
|
126
|
+
* @param {object} param.obj usually the incoming message
|
|
127
|
+
* @param {string} param.prop obj[prop] is containing user-controlled data
|
|
128
|
+
*/
|
|
129
|
+
handle({ obj, prop }) {
|
|
130
|
+
this.typeKey = prop;
|
|
131
|
+
|
|
132
|
+
if (this.skipEvent) return;
|
|
133
|
+
|
|
134
|
+
this.traverseAndTrack({ obj, key: prop });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line complexity
|
|
138
|
+
traverseAndTrack({ obj, key, path = [] }) {
|
|
139
|
+
const value = obj[key];
|
|
140
|
+
|
|
141
|
+
if (!value) return;
|
|
142
|
+
|
|
143
|
+
if (this.sourceEventCount > SOURCE_EVENT_MAX) {
|
|
144
|
+
logger.debug('max sources exceeded for %s', this.type);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const limit = Math.min(value.length, this.samplingThreshold);
|
|
150
|
+
|
|
151
|
+
trackedObjects.set(value, { ...this, path, key });
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < limit; i += this.samplingInterval) {
|
|
154
|
+
this.traverseAndTrack({
|
|
155
|
+
obj: value,
|
|
156
|
+
key: i,
|
|
157
|
+
path: [...path, i]
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
} else if (Buffer.isBuffer(value)) {
|
|
161
|
+
this.sourceEventCount++;
|
|
162
|
+
patcher.patch(value, 'toString', {
|
|
163
|
+
name: 'Buffer.toString',
|
|
164
|
+
alwaysRun: true,
|
|
165
|
+
patchType: PATCH_TYPES.MISC,
|
|
166
|
+
post: (wrapCtx) => {
|
|
167
|
+
this.trackStringProp({
|
|
168
|
+
obj: wrapCtx,
|
|
169
|
+
key: 'result',
|
|
170
|
+
path
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
} else if (typeof value === 'string' || value instanceof String) {
|
|
175
|
+
this.sourceEventCount++;
|
|
176
|
+
this.trackStringProp({ obj, key, path });
|
|
177
|
+
} else if (typeof value === 'object') {
|
|
178
|
+
trackedObjects.set(value, { ...this, path, key });
|
|
179
|
+
|
|
180
|
+
for (const objKey of Object.keys(value)) {
|
|
181
|
+
this.traverseAndTrack({
|
|
182
|
+
obj: value,
|
|
183
|
+
path: [...path, objKey],
|
|
184
|
+
key: objKey
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
trackStringProp({ obj, key, path, value = obj[key] }) {
|
|
191
|
+
const trackData = tracker.track(value);
|
|
192
|
+
if (!trackData) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const name = path.join('.');
|
|
197
|
+
const { props, str: result } = trackData;
|
|
198
|
+
|
|
199
|
+
props.tagRanges = this.getTagRanges({ name, result });
|
|
200
|
+
props.event = new SourceEvent({
|
|
201
|
+
context: new CallContext({
|
|
202
|
+
obj: 'IncomingMessage {}',
|
|
203
|
+
args: [`${this.typeKey}.${name}`],
|
|
204
|
+
result,
|
|
205
|
+
stackOpts: this.stackOpts
|
|
206
|
+
}),
|
|
207
|
+
code: `req.${this.typeKey}.${name}`,
|
|
208
|
+
signature: this.signature,
|
|
209
|
+
tagRanges: props.tagRanges,
|
|
210
|
+
target: 'R',
|
|
211
|
+
type: this.type,
|
|
212
|
+
name
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// NOTE: this used to happen on access in SourceMembrane.onString(), though we
|
|
216
|
+
// should be able to determine access of source value during dataflow - when a
|
|
217
|
+
// source is tracked into PROPAGATION or SINK event.
|
|
218
|
+
storeSource({ type: this.type, name: key });
|
|
219
|
+
|
|
220
|
+
obj[key] = result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getTagRanges({ result, name }) {
|
|
224
|
+
const stop = result.length - 1;
|
|
225
|
+
const tagRanges = [new TagRange(0, stop, 'untrusted')];
|
|
226
|
+
|
|
227
|
+
const typeLowerCase = this.type.toLowerCase();
|
|
228
|
+
const nameLowerCase = name.toLocaleLowerCase();
|
|
229
|
+
|
|
230
|
+
if (typeLowerCase === 'header' && nameLowerCase !== 'referer') {
|
|
231
|
+
tagRanges.push(new TagRange(0, stop, 'header'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeLowerCase === 'cookie') {
|
|
235
|
+
tagRanges.push(new TagRange(0, stop, 'cookie'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this.inputExclusions.length) {
|
|
239
|
+
return tagRanges;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rules = new Set();
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
Maybe it's pointless because we set skipEvent to true if appliesToAllAssessRules
|
|
246
|
+
and we won't get here
|
|
247
|
+
*/
|
|
248
|
+
const exclusions = this.inputExclusions.filter(
|
|
249
|
+
(e) => !e.appliesToAllAssessRules()
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
for (const exclusion of exclusions) {
|
|
253
|
+
if (!exclusion.matches(name)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const ruleId of exclusion.assessmentRulesList) {
|
|
258
|
+
logger.debug(
|
|
259
|
+
'excluding %s %s value from %s (%s)',
|
|
260
|
+
this.type,
|
|
261
|
+
name,
|
|
262
|
+
ruleId,
|
|
263
|
+
exclusion.name
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!rules.has(ruleId)) {
|
|
267
|
+
tagRanges.push(new TagRange(0, stop, `exclusion:${ruleId}`));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
rules.add(ruleId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return tagRanges;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function storeSource({ type, name }) {
|
|
279
|
+
let storedSources = AsyncStorage.get(KEYS.SOURCES);
|
|
280
|
+
|
|
281
|
+
if (!storedSources) {
|
|
282
|
+
// if this is the first source stored on the request, initialize a map
|
|
283
|
+
// and set the storedSources to be a reference to the new, empty map.
|
|
284
|
+
storedSources = new Map();
|
|
285
|
+
AsyncStorage.set(KEYS.SOURCES, storedSources);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// there are cases where AsyncStorage context is out of sync, add defensive code so we don't
|
|
289
|
+
// crash.
|
|
290
|
+
if (storedSources) {
|
|
291
|
+
// save event for route coverage (data pulled in for RouteInfo object)
|
|
292
|
+
// only want to save unique values
|
|
293
|
+
storedSources.set(`${type}-${name}`, new TraceEventSource({ type, name }));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function logExclusionMessage(exclusion, type) {
|
|
298
|
+
const { name } = exclusion;
|
|
299
|
+
logger.debug(
|
|
300
|
+
'excluding %s inputs from all assess rules (%s)',
|
|
301
|
+
type.toLowerCase(),
|
|
302
|
+
name
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports.SourceEventHandler = SourceEventHandler;
|
|
307
|
+
module.exports.trackedObjects = trackedObjects;
|
|
@@ -26,9 +26,12 @@ const parseurl = require('parseurl');
|
|
|
26
26
|
const {
|
|
27
27
|
EXCLUSION_INPUT_TYPES: { BODY, HEADER, PARAMETER, QUERYSTRING, COOKIE }
|
|
28
28
|
} = require('../../constants');
|
|
29
|
+
const { Signature } = require('../models');
|
|
29
30
|
|
|
30
31
|
const sources = module.exports;
|
|
31
32
|
|
|
33
|
+
const { SourceEventHandler } = require('./event-handler');
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Registers sources for assess instrumentation
|
|
34
37
|
*/
|
|
@@ -60,19 +63,104 @@ sources.track = function(type, parent, key, membrane) {
|
|
|
60
63
|
*/
|
|
61
64
|
sources.registerListeners = function({ config, exclusions }) {
|
|
62
65
|
agentEmitter.on('assess.body', (obj, prop) => {
|
|
63
|
-
|
|
66
|
+
if (!config.agent.traverse_and_track) {
|
|
67
|
+
return sources.handleSourceEvent(config, exclusions, BODY, obj, prop);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
agentEmitter.emit('assess.source', {
|
|
71
|
+
config,
|
|
72
|
+
exclusions,
|
|
73
|
+
obj,
|
|
74
|
+
prop,
|
|
75
|
+
data: obj[prop],
|
|
76
|
+
type: BODY
|
|
77
|
+
});
|
|
64
78
|
});
|
|
65
79
|
agentEmitter.on('assess.headers', (obj, prop) => {
|
|
66
|
-
|
|
80
|
+
if (!config.agent.traverse_and_track) {
|
|
81
|
+
return sources.handleSourceEvent(config, exclusions, HEADER, obj, prop);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
agentEmitter.emit('assess.source', {
|
|
85
|
+
obj,
|
|
86
|
+
prop,
|
|
87
|
+
data: obj[prop],
|
|
88
|
+
type: HEADER
|
|
89
|
+
});
|
|
67
90
|
});
|
|
68
91
|
agentEmitter.on('assess.params', (obj, prop) => {
|
|
69
|
-
|
|
92
|
+
if (!config.agent.traverse_and_track) {
|
|
93
|
+
return sources.handleSourceEvent(
|
|
94
|
+
config,
|
|
95
|
+
exclusions,
|
|
96
|
+
PARAMETER,
|
|
97
|
+
obj,
|
|
98
|
+
prop
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
agentEmitter.emit('assess.source', {
|
|
103
|
+
obj,
|
|
104
|
+
prop,
|
|
105
|
+
data: obj[prop],
|
|
106
|
+
type: PARAMETER
|
|
107
|
+
});
|
|
70
108
|
});
|
|
71
109
|
agentEmitter.on('assess.query', (obj, prop) => {
|
|
72
|
-
|
|
110
|
+
if (!config.agent.traverse_and_track) {
|
|
111
|
+
return sources.handleSourceEvent(
|
|
112
|
+
config,
|
|
113
|
+
exclusions,
|
|
114
|
+
QUERYSTRING,
|
|
115
|
+
obj,
|
|
116
|
+
prop
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
agentEmitter.emit('assess.source', {
|
|
121
|
+
obj,
|
|
122
|
+
prop,
|
|
123
|
+
data: obj[prop],
|
|
124
|
+
type: QUERYSTRING
|
|
125
|
+
});
|
|
73
126
|
});
|
|
127
|
+
|
|
74
128
|
agentEmitter.on('assess.cookies', (obj, prop) => {
|
|
75
|
-
|
|
129
|
+
if (!config.agent.traverse_and_track) {
|
|
130
|
+
return sources.handleSourceEvent(config, exclusions, COOKIE, obj, prop);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
agentEmitter.emit('assess.source', {
|
|
134
|
+
obj,
|
|
135
|
+
prop,
|
|
136
|
+
type: COOKIE
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// might be helpful for clients to send add'l values in event arg
|
|
141
|
+
// - stackOpts: elide frames from function ref in client instrumentation
|
|
142
|
+
// - signature: rather than create shared one in the handler
|
|
143
|
+
// - or stack snapshot function - could share among SourceEvents
|
|
144
|
+
// - call context to share among SourceEvents
|
|
145
|
+
agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
|
|
146
|
+
if (!signature) {
|
|
147
|
+
signature = new Signature({
|
|
148
|
+
moduleName: 'Object',
|
|
149
|
+
methodName: 'getter',
|
|
150
|
+
args: [prop],
|
|
151
|
+
return: 'String',
|
|
152
|
+
isModule: false
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
new SourceEventHandler({
|
|
157
|
+
config,
|
|
158
|
+
exclusions,
|
|
159
|
+
signature,
|
|
160
|
+
type,
|
|
161
|
+
stackOpts: undefined,
|
|
162
|
+
snapshot: undefined
|
|
163
|
+
}).handle({ obj, prop });
|
|
76
164
|
});
|
|
77
165
|
};
|
|
78
166
|
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const AssessSinks = require('./sinks');
|
|
18
|
+
|
|
19
|
+
module.exports = class SpdyInstrumentation {
|
|
20
|
+
constructor(agent) {
|
|
21
|
+
new AssessSinks(agent);
|
|
22
|
+
}
|
|
23
|
+
};
|