@contrast/protect 1.6.3 → 1.7.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/fastify.js +1 -1
- package/lib/error-handlers/install/hapi.js +1 -1
- package/lib/index.d.ts +4 -5
- package/lib/input-analysis/handlers.js +43 -4
- package/lib/input-analysis/install/busboy1.js +55 -0
- package/lib/input-analysis/install/formidable1.js +23 -14
- package/lib/input-analysis/install/hapi.js +25 -0
- package/lib/input-analysis/install/multer1.js +21 -3
- package/lib/semantic-analysis/constants.js +20 -0
- package/lib/semantic-analysis/handlers.js +18 -3
- package/lib/semantic-analysis/index.js +9 -0
- package/lib/semantic-analysis/install/libxmljs.js +82 -0
- package/lib/semantic-analysis/utils/xml-analysis.js +107 -0
- package/package.json +5 -8
|
@@ -74,7 +74,7 @@ module.exports = function(core) {
|
|
|
74
74
|
* Instruments fastify in order to add our custom error handler.
|
|
75
75
|
*/
|
|
76
76
|
fastifyErrorHandler.install = function() {
|
|
77
|
-
depHooks.resolve({ name: 'fastify', version: '
|
|
77
|
+
depHooks.resolve({ name: 'fastify', version: '>=3 <5' }, (fastify) => patcher.patch(fastify, {
|
|
78
78
|
name: 'fastify',
|
|
79
79
|
patchType,
|
|
80
80
|
post(data) {
|
|
@@ -39,7 +39,7 @@ module.exports = function (core) {
|
|
|
39
39
|
const isSecurityException = SecurityException.isSecurityException(err);
|
|
40
40
|
|
|
41
41
|
if (isSecurityException && sourceContext && err.output.statusCode !== 403) {
|
|
42
|
-
const [
|
|
42
|
+
const [mode, ruleId] = sourceContext.findings.securityException;
|
|
43
43
|
|
|
44
44
|
err.output.statusCode = 403;
|
|
45
45
|
err.reformat();
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/*
|
|
3
2
|
* Copyright: 2022 Contrast Security, Inc
|
|
4
3
|
* Contact: support@contrastsecurity.com
|
|
@@ -14,7 +13,6 @@
|
|
|
14
13
|
* way not consistent with the End User License Agreement.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
// import { Core } from '@contrast/core';
|
|
18
16
|
import { Logger } from '@contrast/logger';
|
|
19
17
|
import { Sources } from '@contrast/scopes';
|
|
20
18
|
import RequireHook from '@contrast/require-hook';
|
|
@@ -39,7 +37,7 @@ export class HttpInstrumentation {
|
|
|
39
37
|
maxBodySize: number;
|
|
40
38
|
installed: boolean;
|
|
41
39
|
|
|
42
|
-
constructor(core:
|
|
40
|
+
constructor(core: any);
|
|
43
41
|
|
|
44
42
|
install(): void;
|
|
45
43
|
uninstall(): void; //NYI
|
|
@@ -81,7 +79,7 @@ export interface Protect {
|
|
|
81
79
|
handleQueryParams: (sourceContext: ProtectRequestStore, queryParams: { [key: string]: any }) => void,
|
|
82
80
|
handleUrlParams: (sourceContext: ProtectRequestStore, urlParams: { [key: string]: any }) => void,
|
|
83
81
|
handleCookies: (sourceContext: ProtectRequestStore, cookies: { [key: string]: any }) => void,
|
|
84
|
-
|
|
82
|
+
handleFileUploadName: (sourceContext: ProtectRequestStore, names: string[]) => void,
|
|
85
83
|
librariesInstrumentation: {
|
|
86
84
|
bodyParser: { install: () => void },
|
|
87
85
|
coBody: { install: () => void },
|
|
@@ -138,7 +136,8 @@ export interface Protect {
|
|
|
138
136
|
},
|
|
139
137
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
138
|
agentLib: any;
|
|
141
|
-
|
|
139
|
+
packageName: string;
|
|
140
|
+
version: string;
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
export default function(core: Core): ProtectMessage;
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
|
|
18
|
+
const { BLOCKING_MODES, simpleTraverse, Rule, isString } = require('@contrast/common');
|
|
19
19
|
const address = require('ipaddr.js');
|
|
20
|
-
const { Rule } = require('@contrast/common');
|
|
21
20
|
|
|
22
21
|
//
|
|
23
22
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
@@ -370,8 +369,48 @@ module.exports = function(core) {
|
|
|
370
369
|
|
|
371
370
|
// was MULTIPART_NAME but maybe we should just call it what it is. it's kind
|
|
372
371
|
// of a dumb rule anyway. but maybe some code actually uses the name provided.
|
|
373
|
-
inputAnalysis.handleFileUploadName = function(sourceContext,
|
|
374
|
-
|
|
372
|
+
inputAnalysis.handleFileUploadName = function(sourceContext, names) {
|
|
373
|
+
const type = agentLib.InputType.MultipartName;
|
|
374
|
+
const { policy } = sourceContext;
|
|
375
|
+
const resultsList = [];
|
|
376
|
+
|
|
377
|
+
if (policy[Rule.UNSAFE_FILE_UPLOAD] === 'off') return;
|
|
378
|
+
|
|
379
|
+
for (const name of names) {
|
|
380
|
+
if (!isString(name)) {
|
|
381
|
+
logger.debug({ filename: name }, 'handleFileUploadName() was called with non-string');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const items = agentLib.scoreAtom(policy.rulesMask, name, type);
|
|
386
|
+
|
|
387
|
+
if (!items) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const item of items) {
|
|
391
|
+
resultsList.push({
|
|
392
|
+
ruleId: item.ruleId,
|
|
393
|
+
inputType: type,
|
|
394
|
+
name: '',
|
|
395
|
+
value: name,
|
|
396
|
+
score: item.score,
|
|
397
|
+
idsList: item.idsList,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// something was found, so create "complete" findings.
|
|
403
|
+
const unsafeFilenameFindings = {
|
|
404
|
+
trackRequest: true,
|
|
405
|
+
securityException: undefined,
|
|
406
|
+
resultsList,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const block = mergeFindings(sourceContext, unsafeFilenameFindings);
|
|
410
|
+
|
|
411
|
+
if (block) {
|
|
412
|
+
core.protect.throwSecurityException(sourceContext);
|
|
413
|
+
}
|
|
375
414
|
};
|
|
376
415
|
|
|
377
416
|
inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
|
|
@@ -0,0 +1,55 @@
|
|
|
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 = (core) => {
|
|
21
|
+
const {
|
|
22
|
+
depHooks,
|
|
23
|
+
patcher,
|
|
24
|
+
protect,
|
|
25
|
+
protect: { inputAnalysis },
|
|
26
|
+
} = core;
|
|
27
|
+
|
|
28
|
+
// Patch `busboy`
|
|
29
|
+
function install() {
|
|
30
|
+
depHooks.resolve({ name: 'busboy' }, (busboy) => {
|
|
31
|
+
patcher.patch(busboy.prototype, 'emit', {
|
|
32
|
+
name: 'busboy.prototype.emit',
|
|
33
|
+
patchType,
|
|
34
|
+
pre({ args }) {
|
|
35
|
+
const sourceContext = protect.getSourceContext('busboy');
|
|
36
|
+
|
|
37
|
+
if (sourceContext) {
|
|
38
|
+
const [eventName, , , fileName] = args;
|
|
39
|
+
|
|
40
|
+
if (eventName === 'file' && fileName) {
|
|
41
|
+
inputAnalysis.handleFileUploadName(sourceContext, [fileName]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const busboy1Instrumentation = inputAnalysis.busboy1Instrumentation = {
|
|
50
|
+
install
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return busboy1Instrumentation;
|
|
54
|
+
};
|
|
55
|
+
|
|
@@ -21,7 +21,6 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
patcher,
|
|
24
|
-
logger,
|
|
25
24
|
protect,
|
|
26
25
|
protect: { inputAnalysis },
|
|
27
26
|
} = core;
|
|
@@ -33,30 +32,40 @@ module.exports = (core) => {
|
|
|
33
32
|
name: 'Formidable.IncomingForm.prototype.parse',
|
|
34
33
|
patchType,
|
|
35
34
|
pre(data) {
|
|
36
|
-
const
|
|
35
|
+
const sourceContext = protect.getSourceContext('formidable');
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
const
|
|
37
|
+
if (sourceContext) {
|
|
38
|
+
const origCb = data.args[1];
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
data.args[1] = function hookedCb(...cbArgs) {
|
|
41
|
+
const [, fields, files] = cbArgs;
|
|
42
42
|
|
|
43
|
-
if (sourceContext) {
|
|
44
43
|
if (fields) {
|
|
45
44
|
sourceContext.parsedBody = fields;
|
|
46
45
|
inputAnalysis.handleParsedBody(sourceContext, fields);
|
|
47
46
|
}
|
|
47
|
+
|
|
48
48
|
if (files) {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const filenames = [];
|
|
50
|
+
|
|
51
|
+
for (const file of Object.values(files)) {
|
|
52
|
+
if (!Array.isArray(file)) {
|
|
53
|
+
if (file.name) filenames.push(file.name);
|
|
54
|
+
} else {
|
|
55
|
+
for (const multiPart of file) {
|
|
56
|
+
if (multiPart.name) filenames.push(multiPart.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
inputAnalysis.handleFileUploadName(sourceContext, filenames);
|
|
51
62
|
}
|
|
52
|
-
}
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
if (origCb && typeof origCb === 'function') {
|
|
65
|
+
origCb.apply(this, cbArgs);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
data.args[1] = hookedCb;
|
|
60
69
|
}
|
|
61
70
|
});
|
|
62
71
|
|
|
@@ -39,6 +39,10 @@ module.exports = (core) => {
|
|
|
39
39
|
{ name: '@hapi/hapi', version: '>=18 <21' },
|
|
40
40
|
registerServerHandler
|
|
41
41
|
);
|
|
42
|
+
depHooks.resolve(
|
|
43
|
+
{ name: '@hapi/pez' },
|
|
44
|
+
registerMultipartHandler
|
|
45
|
+
);
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const registerServerHandler = (hapi) => {
|
|
@@ -98,6 +102,27 @@ module.exports = (core) => {
|
|
|
98
102
|
});
|
|
99
103
|
};
|
|
100
104
|
|
|
105
|
+
const registerMultipartHandler = (Pez) => {
|
|
106
|
+
patcher.patch(Pez.Dispenser.prototype, 'emit', {
|
|
107
|
+
name: 'Pez.Dispenser.prototype.emit',
|
|
108
|
+
patchType,
|
|
109
|
+
pre: ({ args }) => {
|
|
110
|
+
const sourceContext = protect.getSourceContext('hapi/pez');
|
|
111
|
+
|
|
112
|
+
if (sourceContext) {
|
|
113
|
+
const [eventName, part] = args;
|
|
114
|
+
// note: this will emit multiple file upload events during larger files.
|
|
115
|
+
// given the domain of this as being part of unsafe-file-upload (which is BAP)
|
|
116
|
+
// this isn't much of a concern.
|
|
117
|
+
|
|
118
|
+
if (eventName === 'part' && part.filename) {
|
|
119
|
+
inputAnalysis.handleFileUploadName(sourceContext, [part.filename]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
101
126
|
const hapiInstrumentation = inputAnalysis.hapiInstrumentation = {
|
|
102
127
|
install
|
|
103
128
|
};
|
|
@@ -48,6 +48,7 @@ module.exports = (core) => {
|
|
|
48
48
|
|
|
49
49
|
function contrastNext(origErr) {
|
|
50
50
|
let securityException;
|
|
51
|
+
const filenames = [];
|
|
51
52
|
|
|
52
53
|
if (req.body) {
|
|
53
54
|
sourceContext.parsedBody = req.body;
|
|
@@ -63,9 +64,26 @@ module.exports = (core) => {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
if (req.file
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
if (req.file) {
|
|
68
|
+
filenames.push(req.file.originalname);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.files) {
|
|
72
|
+
for (const file of Object.values(req.files)) {
|
|
73
|
+
filenames.push(file.originalname);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (filenames.length) {
|
|
78
|
+
try {
|
|
79
|
+
inputAnalysis.handleFileUploadName(sourceContext, filenames);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (isSecurityException(err)) {
|
|
82
|
+
securityException = err;
|
|
83
|
+
} else {
|
|
84
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
const error = securityException || origErr;
|
|
@@ -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-semantic-analysis',
|
|
20
|
+
};
|
|
@@ -20,10 +20,13 @@ const {
|
|
|
20
20
|
BLOCKING_MODES,
|
|
21
21
|
ProtectRuleMode: { OFF },
|
|
22
22
|
InputType,
|
|
23
|
-
isString,
|
|
24
23
|
simpleTraverse
|
|
25
24
|
} = require('@contrast/common');
|
|
26
25
|
|
|
26
|
+
const {
|
|
27
|
+
findExternalEntities,
|
|
28
|
+
} = require('./utils/xml-analysis');
|
|
29
|
+
|
|
27
30
|
const SINK_EXPLOIT_PATTERN_START = /(?:^|\\|\/)(?:sh|bash|zsh|ksh|tcsh|csh|fish|cmd)/;
|
|
28
31
|
const stripWhiteSpace = (str) => str.replace(/\s/g, '');
|
|
29
32
|
|
|
@@ -103,6 +106,18 @@ module.exports = function(core) {
|
|
|
103
106
|
}
|
|
104
107
|
};
|
|
105
108
|
|
|
109
|
+
semanticAnalysis.handleXXE = function (sourceContext, sinkContext) {
|
|
110
|
+
const mode = sourceContext.policy[Rule.XXE];
|
|
111
|
+
if (mode == OFF) return;
|
|
112
|
+
|
|
113
|
+
const findings = findExternalEntities(sinkContext.value);
|
|
114
|
+
if (findings.entities.length) {
|
|
115
|
+
handleResult(sourceContext, sinkContext, Rule.XXE, mode, {
|
|
116
|
+
findings,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
106
121
|
return semanticAnalysis;
|
|
107
122
|
};
|
|
108
123
|
|
|
@@ -127,7 +142,7 @@ function findBackdoorInjection(sourceContext, command) {
|
|
|
127
142
|
};
|
|
128
143
|
|
|
129
144
|
let found = false;
|
|
130
|
-
for (
|
|
145
|
+
for (const inputType in valuesOfInterest) {
|
|
131
146
|
const values = valuesOfInterest[inputType];
|
|
132
147
|
|
|
133
148
|
if (values && Object.keys(values).length) {
|
|
@@ -139,7 +154,7 @@ function findBackdoorInjection(sourceContext, command) {
|
|
|
139
154
|
) {
|
|
140
155
|
let key;
|
|
141
156
|
if (inputType === InputType.HEADER) {
|
|
142
|
-
key = obj[path[0] - 1]
|
|
157
|
+
key = obj[path[0] - 1];
|
|
143
158
|
} else {
|
|
144
159
|
key = path[path.length - 1];
|
|
145
160
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { installChildComponentsSync } = require('@contrast/common');
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* SEMANTIC ANALYSIS is a STAGE of Protect.
|
|
20
22
|
* The information about it can be found under some of the separate rules we support for this stage:
|
|
@@ -31,6 +33,13 @@ module.exports = function(core) {
|
|
|
31
33
|
// load the interfaces that will be used by input tracing instrumentation
|
|
32
34
|
require('./handlers')(core);
|
|
33
35
|
|
|
36
|
+
// instrumentation
|
|
37
|
+
require('./install/libxmljs')(core);
|
|
38
|
+
|
|
39
|
+
semanticAnalysis.install = function() {
|
|
40
|
+
installChildComponentsSync(semanticAnalysis);
|
|
41
|
+
};
|
|
42
|
+
|
|
34
43
|
// There is no `.install()` method as this STAGE does not introduce side effects on its own,
|
|
35
44
|
// it uses the instrumentation that's already in place for INPUT TRACING.
|
|
36
45
|
|
|
@@ -0,0 +1,82 @@
|
|
|
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 SecurityException = require('../../security-exception');
|
|
20
|
+
const { patchType } = require('../constants');
|
|
21
|
+
|
|
22
|
+
module.exports = function(core) {
|
|
23
|
+
const {
|
|
24
|
+
depHooks,
|
|
25
|
+
patcher,
|
|
26
|
+
logger,
|
|
27
|
+
protect: { semanticAnalysis },
|
|
28
|
+
protect,
|
|
29
|
+
captureStacktrace
|
|
30
|
+
} = core;
|
|
31
|
+
|
|
32
|
+
function install() {
|
|
33
|
+
depHooks.resolve({ name: 'libxmljs' }, hookLibXml);
|
|
34
|
+
// libxmljs2 is a fork of libxml that has identical signatures.
|
|
35
|
+
// the only difference is that libxmljs2 is used by juice-shop and thus
|
|
36
|
+
// we're missing sinks in one of our most important sample apps.
|
|
37
|
+
depHooks.resolve({ name: 'libxmljs2' }, hookLibXml);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hookLibXml(libxmljs) {
|
|
41
|
+
patcher.patch(libxmljs, 'parseXmlString', {
|
|
42
|
+
name: 'libxmljs.parseXmlString',
|
|
43
|
+
patchType,
|
|
44
|
+
pre: preParseXmlMethod
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
patcher.patch(libxmljs, 'parseXml', {
|
|
48
|
+
name: 'libxmljs.parseXml',
|
|
49
|
+
patchType,
|
|
50
|
+
pre: preParseXmlMethod
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function preParseXmlMethod({ args, hooked, orig }) {
|
|
55
|
+
const sourceContext = protect.getSourceContext('libxmljs.parseXmlString');
|
|
56
|
+
const value = args[0];
|
|
57
|
+
|
|
58
|
+
// If noent isn't set to true than libxml won't load
|
|
59
|
+
// external entities, so this isn't vulnerable
|
|
60
|
+
// see: https://help.semmle.com/wiki/display/JS/XML+external+entity+expansion
|
|
61
|
+
if (!sourceContext || !value || !isString(value) || !args[1].noent) return;
|
|
62
|
+
|
|
63
|
+
const sinkContext = captureStacktrace(
|
|
64
|
+
{ name: 'libxmljs.parseXmlString', value },
|
|
65
|
+
{ constructorOpt: hooked, prependFrames: [orig] }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
semanticAnalysis.handleXXE(sourceContext, sinkContext);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (SecurityException.isSecurityException(err)) {
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.error({ err }, 'Unexpected error during semantic analysis');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const libxmljs = semanticAnalysis.libxmljs = { install };
|
|
80
|
+
|
|
81
|
+
return libxmljs;
|
|
82
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
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 PROTOCOLS = {
|
|
18
|
+
FTP: 'FTP',
|
|
19
|
+
HTTP: 'HTTP',
|
|
20
|
+
HTTPS: 'HTTPS',
|
|
21
|
+
TCP: 'TCP'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const FTP = `${PROTOCOLS.FTP.toLowerCase()}:`;
|
|
25
|
+
const HTTP = `${PROTOCOLS.HTTP.toLowerCase()}:`;
|
|
26
|
+
const HTTPS = `${PROTOCOLS.HTTPS.toLowerCase()}:`;
|
|
27
|
+
const DTD_EXTENSION = '.dtd';
|
|
28
|
+
const FILE_START = 'file:';
|
|
29
|
+
const GOPHER_START = 'gopher:';
|
|
30
|
+
const JAR_START = 'jar:';
|
|
31
|
+
const DOT = '.';
|
|
32
|
+
const FORWARD_SLASH = '/';
|
|
33
|
+
const UP_DIR_LINUX = '../';
|
|
34
|
+
const UP_DIR_WIN = '..\\';
|
|
35
|
+
const ENTITY_TYPES = {
|
|
36
|
+
SYSTEM: 'SYSTEM',
|
|
37
|
+
PUBLIC: 'PUBLIC'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// We only use this against lowercase strings; removed A-Z for speed
|
|
41
|
+
const FILE_PATTERN_WINDOWS = /^[\\\\]*[a-z]{1,3}:.*/;
|
|
42
|
+
|
|
43
|
+
const entityRegex = /<!ENTITY\s+(?<name>[a-zA-Z0-f]+)\s+(?<type>SYSTEM|PUBLIC)\s+"(?<uri1>.*?)"\s*("(?<uri2>.*?)"\s*)?>/g;
|
|
44
|
+
|
|
45
|
+
// Helper Functions
|
|
46
|
+
const indicators = [
|
|
47
|
+
FTP,
|
|
48
|
+
FILE_START,
|
|
49
|
+
JAR_START,
|
|
50
|
+
GOPHER_START,
|
|
51
|
+
FORWARD_SLASH,
|
|
52
|
+
DOT,
|
|
53
|
+
UP_DIR_LINUX,
|
|
54
|
+
UP_DIR_WIN,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function isExternalEntity(str) {
|
|
58
|
+
if (!str) return false;
|
|
59
|
+
|
|
60
|
+
if (str.startsWith(HTTP) || str.startsWith(HTTPS)) {
|
|
61
|
+
if (!str.endsWith(DTD_EXTENSION)) return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const val of indicators) {
|
|
65
|
+
if (str.startsWith(val)) return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return FILE_PATTERN_WINDOWS.test(str);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {String} xml
|
|
73
|
+
*/
|
|
74
|
+
module.exports.findExternalEntities = function(xml = '') {
|
|
75
|
+
const entities = [];
|
|
76
|
+
let match;
|
|
77
|
+
|
|
78
|
+
while ((match = entityRegex.exec(xml))) {
|
|
79
|
+
const {
|
|
80
|
+
groups: { type, uri1, uri2 }
|
|
81
|
+
} = match;
|
|
82
|
+
|
|
83
|
+
let uri;
|
|
84
|
+
if (type === ENTITY_TYPES.SYSTEM && isExternalEntity(uri1)) {
|
|
85
|
+
uri = uri1;
|
|
86
|
+
} else if (type === ENTITY_TYPES.PUBLIC && isExternalEntity(uri2)) {
|
|
87
|
+
uri = uri2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
uri && entities.push({
|
|
91
|
+
start: match.index,
|
|
92
|
+
finish: match.index + match[0].length,
|
|
93
|
+
type,
|
|
94
|
+
uri,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const len = entities.length;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
entities,
|
|
102
|
+
prolog: len && xml.substr(0, entities[len - 1].finish) || null
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports.ENTITY_TYPES = ENTITY_TYPES;
|
|
107
|
+
module.exports.isExternalEntity = isExternalEntity;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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,14 +17,11 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@babel/template": "^7.16.7",
|
|
21
|
-
"@babel/types": "^7.16.8",
|
|
22
20
|
"@contrast/agent-lib": "^5.1.0",
|
|
23
|
-
"@contrast/common": "1.1.
|
|
24
|
-
"@contrast/core": "1.
|
|
25
|
-
"@contrast/esm-hooks": "1.
|
|
26
|
-
"@contrast/scopes": "1.
|
|
27
|
-
"builtin-modules": "^3.2.0",
|
|
21
|
+
"@contrast/common": "1.1.4",
|
|
22
|
+
"@contrast/core": "1.7.0",
|
|
23
|
+
"@contrast/esm-hooks": "1.3.0",
|
|
24
|
+
"@contrast/scopes": "1.2.0",
|
|
28
25
|
"ipaddr.js": "^2.0.1",
|
|
29
26
|
"semver": "^7.3.7"
|
|
30
27
|
}
|