@contrast/assess 1.1.0 → 1.2.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/dataflow/propagation/index.js +6 -7
- package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +56 -0
- package/lib/dataflow/propagation/install/string/index.js +1 -0
- package/lib/dataflow/propagation/install/string/split.js +108 -0
- package/lib/dataflow/sources/index.js +0 -1
- package/lib/dataflow/sources/install/http.js +9 -6
- package/lib/index.js +7 -2
- package/lib/response-scanning/handlers/index.js +274 -0
- package/lib/response-scanning/handlers/utils.js +394 -0
- package/lib/response-scanning/index.js +36 -0
- package/lib/response-scanning/install/http.js +119 -0
- package/package.json +3 -3
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -266
- package/coverage/lcov-report/lib/dataflow/common.js.html +0 -154
- package/coverage/lcov-report/lib/dataflow/event-factory.js.html +0 -598
- package/coverage/lcov-report/lib/dataflow/index.html +0 -191
- package/coverage/lcov-report/lib/dataflow/index.js.html +0 -190
- package/coverage/lcov-report/lib/dataflow/propagation/common.js.html +0 -145
- package/coverage/lcov-report/lib/dataflow/propagation/index.html +0 -131
- package/coverage/lcov-report/lib/dataflow/propagation/index.js.html +0 -190
- package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/index.html +0 -131
- package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/index.js.html +0 -184
- package/coverage/lcov-report/lib/dataflow/propagation/install/contrast-methods/plus.js.html +0 -397
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/concat.js.html +0 -478
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/index.html +0 -176
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/index.js.html +0 -202
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/replace.js.html +0 -496
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/substring.js.html +0 -415
- package/coverage/lcov-report/lib/dataflow/propagation/install/string/trim.js.html +0 -403
- package/coverage/lcov-report/lib/dataflow/signatures.js.html +0 -5923
- package/coverage/lcov-report/lib/dataflow/sinks/common.js.html +0 -145
- package/coverage/lcov-report/lib/dataflow/sinks/index.html +0 -131
- package/coverage/lcov-report/lib/dataflow/sinks/index.js.html +0 -211
- package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/index.html +0 -146
- package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/index.js.html +0 -172
- package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js.html +0 -319
- package/coverage/lcov-report/lib/dataflow/sinks/install/fastify/xss.js.html +0 -721
- package/coverage/lcov-report/lib/dataflow/sinks/install/http.js.html +0 -340
- package/coverage/lcov-report/lib/dataflow/sinks/install/index.html +0 -116
- package/coverage/lcov-report/lib/dataflow/sources/common.js.html +0 -145
- package/coverage/lcov-report/lib/dataflow/sources/index.html +0 -131
- package/coverage/lcov-report/lib/dataflow/sources/index.js.html +0 -226
- package/coverage/lcov-report/lib/dataflow/sources/install/fastify.js.html +0 -379
- package/coverage/lcov-report/lib/dataflow/sources/install/http.js.html +0 -502
- package/coverage/lcov-report/lib/dataflow/sources/install/index.html +0 -146
- package/coverage/lcov-report/lib/dataflow/sources/install/qs.js.html +0 -322
- package/coverage/lcov-report/lib/dataflow/tag-utils.js.html +0 -418
- package/coverage/lcov-report/lib/dataflow/tracker.js.html +0 -466
- package/coverage/lcov-report/lib/dataflow/utils/index.html +0 -116
- package/coverage/lcov-report/lib/dataflow/utils/is-vulnerable.js.html +0 -424
- package/coverage/lcov-report/lib/index.html +0 -116
- package/coverage/lcov-report/lib/index.js.html +0 -193
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov.info +0 -4856
- package/coverage/tmp/coverage-9548-1675168551025-0.json +0 -1
- package/coverage/tmp/coverage-9551-1675168550963-0.json +0 -1
- package/coverage/tmp/coverage-9552-1675168550969-0.json +0 -1
- package/coverage/tmp/coverage-9553-1675168550970-0.json +0 -1
- package/coverage/tmp/coverage-9554-1675168550962-0.json +0 -1
- package/coverage/tmp/coverage-9555-1675168550965-0.json +0 -1
- package/coverage/tmp/coverage-9556-1675168550964-0.json +0 -1
- package/coverage/tmp/coverage-9557-1675168550969-0.json +0 -1
- package/coverage/tmp/coverage-9558-1675168550964-0.json +0 -1
- package/coverage/tmp/coverage-9559-1675168550971-0.json +0 -1
- package/lib/dataflow/sources/install/qs.js +0 -88
|
@@ -20,15 +20,15 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const propagation = core.assess.dataflow.propagation = {};
|
|
22
22
|
|
|
23
|
-
require('./install/string')(core);
|
|
24
|
-
require('./install/array-prototype-join')(core);
|
|
25
|
-
require('./install/pug')(core);
|
|
26
23
|
require('./install/contrast-methods')(core);
|
|
27
|
-
require('./install/validator')(core);
|
|
28
|
-
require('./install/url')(core);
|
|
29
24
|
require('./install/ejs')(core);
|
|
30
|
-
require('./install/
|
|
25
|
+
require('./install/pug')(core);
|
|
26
|
+
require('./install/string')(core);
|
|
27
|
+
require('./install/url')(core);
|
|
28
|
+
require('./install/validator')(core);
|
|
29
|
+
require('./install/array-prototype-join')(core);
|
|
31
30
|
require('./install/decode-uri-component')(core);
|
|
31
|
+
require('./install/encode-uri-component')(core);
|
|
32
32
|
require('./install/escape-html')(core);
|
|
33
33
|
require('./install/escape')(core);
|
|
34
34
|
require('./install/handlebars-utils-escape-expression')(core);
|
|
@@ -37,7 +37,6 @@ module.exports = function(core) {
|
|
|
37
37
|
require('./install/sql-template-strings')(core);
|
|
38
38
|
require('./install/unescape')(core);
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
propagation.install = function() {
|
|
42
41
|
callChildComponentMethodsSync(propagation, 'install');
|
|
43
42
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
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('../../common');
|
|
19
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const {
|
|
22
|
+
scopes: { sources, instrumentation },
|
|
23
|
+
patcher,
|
|
24
|
+
assess: {
|
|
25
|
+
dataflow: { tracker }
|
|
26
|
+
}
|
|
27
|
+
} = core;
|
|
28
|
+
|
|
29
|
+
return core.assess.dataflow.propagation.contrastMethodsInstrumentation.string = {
|
|
30
|
+
install() {
|
|
31
|
+
patcher.patch(global.ContrastMethods, 'String', {
|
|
32
|
+
name: 'ContrastMethods.String',
|
|
33
|
+
patchType,
|
|
34
|
+
post(data) {
|
|
35
|
+
if (!data.result || !sources.getStore() || instrumentation.isLocked()) return;
|
|
36
|
+
|
|
37
|
+
const arg = data.args[0];
|
|
38
|
+
let argInfo = tracker.getData(arg);
|
|
39
|
+
|
|
40
|
+
if (data.obj && !argInfo) {
|
|
41
|
+
argInfo = tracker.getData(data.result.toString());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!arg || !argInfo) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { extern } = tracker.track(data.result, argInfo);
|
|
49
|
+
if (extern) {
|
|
50
|
+
data.result = extern;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
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('../../common');
|
|
19
|
+
const { join } = require('@contrast/common');
|
|
20
|
+
const { createSubsetTags } = require('../../../tag-utils');
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
module.exports = function(core) {
|
|
24
|
+
const {
|
|
25
|
+
scopes: { sources, instrumentation },
|
|
26
|
+
patcher,
|
|
27
|
+
assess: {
|
|
28
|
+
dataflow: { tracker, eventFactory: { createPropagationEvent } }
|
|
29
|
+
}
|
|
30
|
+
} = core;
|
|
31
|
+
|
|
32
|
+
return core.assess.dataflow.propagation.stringInstrumentation.split = {
|
|
33
|
+
install() {
|
|
34
|
+
const name = 'String.prototype.split';
|
|
35
|
+
|
|
36
|
+
patcher.patch(String.prototype, 'split', {
|
|
37
|
+
name,
|
|
38
|
+
patchType,
|
|
39
|
+
post(data) {
|
|
40
|
+
const { name, args, obj, result, hooked, orig } = data;
|
|
41
|
+
if (
|
|
42
|
+
!obj ||
|
|
43
|
+
!result ||
|
|
44
|
+
args.length === 0 ||
|
|
45
|
+
result.length === 0 ||
|
|
46
|
+
!sources.getStore() ||
|
|
47
|
+
typeof obj !== 'string' ||
|
|
48
|
+
instrumentation.isLocked() ||
|
|
49
|
+
(args.length === 1 && args[0] == null)
|
|
50
|
+
) return;
|
|
51
|
+
|
|
52
|
+
const objInfo = tracker.getData(obj);
|
|
53
|
+
if (!objInfo) return;
|
|
54
|
+
|
|
55
|
+
let idx = 0;
|
|
56
|
+
for (let i = 0; i < result.length; i++) {
|
|
57
|
+
const res = result[i];
|
|
58
|
+
const start = obj.indexOf(res, idx);
|
|
59
|
+
idx += res.length;
|
|
60
|
+
const objSubstr = obj.substring(start, start + res.length);
|
|
61
|
+
const objSubstrInfo = tracker.getData(objSubstr);
|
|
62
|
+
if (objSubstrInfo) {
|
|
63
|
+
const tags = createSubsetTags(objInfo.tags, start, res.length - 1);
|
|
64
|
+
if (!tags) continue;
|
|
65
|
+
|
|
66
|
+
const event = createPropagationEvent({
|
|
67
|
+
name,
|
|
68
|
+
history: [objInfo],
|
|
69
|
+
object: {
|
|
70
|
+
value: obj,
|
|
71
|
+
isTracked: true,
|
|
72
|
+
},
|
|
73
|
+
args: args.map((arg) => {
|
|
74
|
+
const argInfo = tracker.getData(arg);
|
|
75
|
+
return {
|
|
76
|
+
value: argInfo ? argInfo.value : arg.toString(),
|
|
77
|
+
isTracked: !!argInfo
|
|
78
|
+
};
|
|
79
|
+
}),
|
|
80
|
+
tags,
|
|
81
|
+
result: {
|
|
82
|
+
value: join(result),
|
|
83
|
+
isTracked: false
|
|
84
|
+
},
|
|
85
|
+
stacktraceOpts: {
|
|
86
|
+
constructorOpt: hooked,
|
|
87
|
+
prependFrames: [orig]
|
|
88
|
+
},
|
|
89
|
+
source: 'O',
|
|
90
|
+
target: 'R'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (event) {
|
|
94
|
+
const { extern } = tracker.track(res, event);
|
|
95
|
+
if (extern) {
|
|
96
|
+
data.result[i] = extern;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
uninstall() {
|
|
105
|
+
String.prototype.split = patcher.unwrap(String.prototype.split);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
const { patchType } = require('../common');
|
|
18
|
+
const { toLowerCase } = require('@contrast/common');
|
|
18
19
|
|
|
19
20
|
// This is only just initiating an async storage for the http source
|
|
20
21
|
// TODO Tracking the user input
|
|
@@ -59,13 +60,13 @@ module.exports = function(core) {
|
|
|
59
60
|
const key = obj[i];
|
|
60
61
|
const value = obj[i + 1];
|
|
61
62
|
|
|
62
|
-
if (
|
|
63
|
+
if (toLowerCase(key) === 'content-type') {
|
|
63
64
|
store.assess.responseData.contentType = value;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
} else if (typeof obj === 'object') {
|
|
67
68
|
for (const [key, value] of Object.entries(obj)) {
|
|
68
|
-
if (
|
|
69
|
+
if (toLowerCase(key) === 'content-type') {
|
|
69
70
|
store.assess.responseData.contentType = value;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -79,7 +80,7 @@ module.exports = function(core) {
|
|
|
79
80
|
patchType,
|
|
80
81
|
pre(data) {
|
|
81
82
|
const [name = '', value] = data.args;
|
|
82
|
-
if (
|
|
83
|
+
if (toLowerCase(name) === 'content-type' && value) {
|
|
83
84
|
store.assess.responseData.contentType = value;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -99,11 +100,11 @@ module.exports = function(core) {
|
|
|
99
100
|
const headers = {};
|
|
100
101
|
|
|
101
102
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
102
|
-
const header = req.rawHeaders[i]
|
|
103
|
+
const header = toLowerCase(req.rawHeaders[i]);
|
|
103
104
|
headers[header] = req.rawHeaders[i + 1];
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
const contentType = headers['content-type']
|
|
107
|
+
const contentType = headers['content-type'] && toLowerCase(headers['content-type']);
|
|
107
108
|
const reqData = {
|
|
108
109
|
ip: req.socket.remoteAddress,
|
|
109
110
|
httpVersion: req.httpVersion,
|
|
@@ -125,7 +126,9 @@ module.exports = function(core) {
|
|
|
125
126
|
logger.error({ err }, 'Error during assess request handling');
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
setImmediate(() => {
|
|
130
|
+
next.call(data.obj, ...data.args);
|
|
131
|
+
});
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
function install() {
|
package/lib/index.js
CHANGED
|
@@ -15,21 +15,26 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
18
19
|
const dataflow = require('./dataflow');
|
|
20
|
+
const responseScanning = require('./response-scanning');
|
|
19
21
|
|
|
20
22
|
module.exports = function assess(core) {
|
|
23
|
+
if (!core.config.assess.enable) return;
|
|
24
|
+
|
|
21
25
|
const assess = core.assess = {};
|
|
22
26
|
|
|
23
27
|
// Does this order matter? Probably not
|
|
24
28
|
// 1. dataflow
|
|
25
29
|
dataflow(core);
|
|
30
|
+
responseScanning(core);
|
|
26
31
|
|
|
27
|
-
// response-scanning
|
|
28
32
|
// crypto
|
|
29
33
|
// static (in coordination with rewriter)
|
|
30
34
|
|
|
31
35
|
assess.install = function() {
|
|
32
|
-
core.
|
|
36
|
+
core.rewriter.install('assess');
|
|
37
|
+
callChildComponentMethodsSync(core.assess, 'install');
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
return assess;
|
|
@@ -0,0 +1,274 @@
|
|
|
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 {
|
|
19
|
+
escapeHtml,
|
|
20
|
+
isHtmlContent,
|
|
21
|
+
getElements,
|
|
22
|
+
getAttribute,
|
|
23
|
+
isParseableResponse,
|
|
24
|
+
checkCacheControlValue,
|
|
25
|
+
checkMetaTags,
|
|
26
|
+
getCspHeaders,
|
|
27
|
+
checkCspSources
|
|
28
|
+
} = require('./utils');
|
|
29
|
+
const { toLowerCase, substring, ResponseScanningRule } = require('@contrast/common');
|
|
30
|
+
|
|
31
|
+
module.exports = function(core) {
|
|
32
|
+
const {
|
|
33
|
+
assess: {
|
|
34
|
+
responseScanning,
|
|
35
|
+
responseScanning: {
|
|
36
|
+
reportFindings
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} = core;
|
|
40
|
+
|
|
41
|
+
responseScanning.handleAutoCompleteMissing = function(sourceContext, { responseHeaders, responseBody }) {
|
|
42
|
+
if (!isHtmlContent(responseHeaders)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const elements = getElements('form', responseBody);
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < elements.length; i++) {
|
|
49
|
+
const autocomplete = getAttribute('autocomplete', elements[i]);
|
|
50
|
+
if (autocomplete !== 'off') {
|
|
51
|
+
reportFindings(sourceContext,
|
|
52
|
+
{
|
|
53
|
+
ruleId: ResponseScanningRule.AUTOCOMPLETE_MISSING,
|
|
54
|
+
vulnerabilityMetadata: {
|
|
55
|
+
attribute: autocomplete,
|
|
56
|
+
html: escapeHtml(elements[i]),
|
|
57
|
+
start: 0,
|
|
58
|
+
end: elements[i].length
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
responseScanning.handleCacheControlsMissing = function(sourceContext, { responseHeaders, responseBody }) {
|
|
67
|
+
const instructions = [];
|
|
68
|
+
|
|
69
|
+
// de-dupe; this will be re-emitted for parseableBody handlers anyway
|
|
70
|
+
if (isParseableResponse(responseHeaders) && !responseBody) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const cacheControlHeader = responseHeaders['cache-control'];
|
|
74
|
+
|
|
75
|
+
// save the Pragma Header
|
|
76
|
+
if (responseHeaders['pragma']) {
|
|
77
|
+
instructions.push({
|
|
78
|
+
type: 'Header',
|
|
79
|
+
name: 'pragma',
|
|
80
|
+
value: responseHeaders['pragma']
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cacheControlHeader) {
|
|
85
|
+
const [containsNoCache, containsNoStore] = checkCacheControlValue(
|
|
86
|
+
cacheControlHeader
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// got everything from header so we don't care about meta tags
|
|
90
|
+
if (containsNoCache && containsNoStore) {
|
|
91
|
+
return;
|
|
92
|
+
} else {
|
|
93
|
+
// save the instructions in case meta tags are missing
|
|
94
|
+
instructions.push({
|
|
95
|
+
type: 'Header',
|
|
96
|
+
name: 'cache-control',
|
|
97
|
+
value: cacheControlHeader
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// we checked the headers now time to check the HTML content
|
|
103
|
+
checkMetaTags({ body: responseBody, cacheControlHeader, instructions });
|
|
104
|
+
|
|
105
|
+
if (instructions.length) {
|
|
106
|
+
reportFindings(sourceContext, {
|
|
107
|
+
ruleId: ResponseScanningRule.CACHE_CONTROLS_MISSING,
|
|
108
|
+
vulnerabilityMetadata: {
|
|
109
|
+
data: JSON.stringify(instructions)
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
responseScanning.handleClickJackingControlsMissing = function(sourceContext, { responseHeaders }) {
|
|
116
|
+
// look for x-frame-options headers with deny or sameorigin
|
|
117
|
+
const xFrameHeaders = responseHeaders['x-frame-options'];
|
|
118
|
+
let hasFrameBusting = false;
|
|
119
|
+
|
|
120
|
+
if (xFrameHeaders) {
|
|
121
|
+
const xFrameHeadersLC = toLowerCase(xFrameHeaders);
|
|
122
|
+
hasFrameBusting =
|
|
123
|
+
xFrameHeadersLC.indexOf('deny') > -1 ||
|
|
124
|
+
xFrameHeadersLC.indexOf('sameorigin') > -1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!hasFrameBusting) {
|
|
128
|
+
reportFindings(sourceContext, { ruleId: ResponseScanningRule.CLICKJACKING_CONTROL_MISSING });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
responseScanning.handleParameterPollution = function(sourceContext, { responseBody }) {
|
|
133
|
+
// look for form tag with missing action attribute.
|
|
134
|
+
// ex: <form method="post">..
|
|
135
|
+
const elements = getElements('form', responseBody);
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < elements.length; i++) {
|
|
138
|
+
const action = getAttribute('action', elements[i]);
|
|
139
|
+
if (!action) {
|
|
140
|
+
reportFindings(sourceContext, {
|
|
141
|
+
ruleId: ResponseScanningRule.PARAMETER_POLLUTION,
|
|
142
|
+
vulnerabilityMetadata: {
|
|
143
|
+
attribute: action,
|
|
144
|
+
html: escapeHtml(elements[i])
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Checks the response headers for the CSP. If found, and insecure, will return
|
|
153
|
+
* the evidence for reporting.
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} responseHeaders - HTTP headers object.
|
|
156
|
+
* @returns {Object} - Evidence for insecure CSP header.
|
|
157
|
+
*/
|
|
158
|
+
responseScanning.handleCspHeader = function(sourceContext, { responseHeaders }) {
|
|
159
|
+
const cspHeaders = getCspHeaders(responseHeaders);
|
|
160
|
+
|
|
161
|
+
// Don't report if not set; this report belongs to 'csp-header-missing'
|
|
162
|
+
if (!cspHeaders) {
|
|
163
|
+
reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_MISSING });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const vulnerabilityMetadata = checkCspSources(cspHeaders);
|
|
168
|
+
|
|
169
|
+
if (vulnerabilityMetadata.insecure) {
|
|
170
|
+
// We do this because TS API will expect these keys (although it is a typo)
|
|
171
|
+
// When they fix the API we can remove this
|
|
172
|
+
vulnerabilityMetadata.refererSecure = vulnerabilityMetadata.referrerSecure;
|
|
173
|
+
vulnerabilityMetadata.refererValue = vulnerabilityMetadata.referrerValue;
|
|
174
|
+
|
|
175
|
+
delete vulnerabilityMetadata.insecure;
|
|
176
|
+
delete vulnerabilityMetadata.referrerSecure;
|
|
177
|
+
delete vulnerabilityMetadata.referrerValue;
|
|
178
|
+
|
|
179
|
+
reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_INSECURE, vulnerabilityMetadata });
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
responseScanning.handleHstsHeaderMissing = function(sourceContext, { responseHeaders }) {
|
|
184
|
+
let header = responseHeaders['strict-transport-security'];
|
|
185
|
+
let maxAge;
|
|
186
|
+
|
|
187
|
+
if (header) {
|
|
188
|
+
header = toLowerCase(header);
|
|
189
|
+
const flag = header.indexOf('max-age');
|
|
190
|
+
if (flag > -1) {
|
|
191
|
+
const equal = header.indexOf('=', flag);
|
|
192
|
+
if (equal > -1) {
|
|
193
|
+
const semicolon = header.indexOf(';', equal);
|
|
194
|
+
if (semicolon > -1) {
|
|
195
|
+
maxAge = substring(header, equal + 1, semicolon);
|
|
196
|
+
} else {
|
|
197
|
+
maxAge = substring(header, equal + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!(parseInt(maxAge) > 0)) {
|
|
204
|
+
reportFindings(sourceContext, {
|
|
205
|
+
ruleId: ResponseScanningRule.HSTS_HEADER_MISSING,
|
|
206
|
+
vulnerabilityMetadata: {
|
|
207
|
+
data: maxAge || ''
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
responseScanning.handlePoweredByHeader = function(sourceContext, { responseHeaders }) {
|
|
214
|
+
const headerName = 'x-powered-by';
|
|
215
|
+
let header = responseHeaders[headerName];
|
|
216
|
+
|
|
217
|
+
if (header) {
|
|
218
|
+
header = toLowerCase(header);
|
|
219
|
+
|
|
220
|
+
const instructions = [
|
|
221
|
+
{
|
|
222
|
+
type: 'Header',
|
|
223
|
+
name: headerName,
|
|
224
|
+
value: header
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
reportFindings(sourceContext, {
|
|
229
|
+
ruleId: ResponseScanningRule.POWERED_BY_HEADER,
|
|
230
|
+
vulnerabilityMetadata: {
|
|
231
|
+
data: JSON.stringify(instructions)
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
responseScanning.handleXContentTypeHeaderMissing = function(sourceContext, { responseHeaders }) {
|
|
238
|
+
const headerName = 'x-content-type-options';
|
|
239
|
+
let header = responseHeaders[headerName];
|
|
240
|
+
|
|
241
|
+
if (header) {
|
|
242
|
+
header = toLowerCase(header);
|
|
243
|
+
if (header === 'nosniff') {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reportFindings(sourceContext, {
|
|
249
|
+
ruleId: ResponseScanningRule.XCONTENTTYPE_HEADER_MISSING,
|
|
250
|
+
vulnerabilityMetadata: {
|
|
251
|
+
data: header || ''
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
responseScanning.handleXxsProtectionHeaderDisabled = function(sourceContext, { responseHeaders }) {
|
|
257
|
+
const header = responseHeaders['x-xss-protection'];
|
|
258
|
+
|
|
259
|
+
// This header is set by default, so `header` should always be present.
|
|
260
|
+
if (header && header.startsWith('1')) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
reportFindings(sourceContext, {
|
|
265
|
+
ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
|
|
266
|
+
vulnerabilityMetadata: {
|
|
267
|
+
data: header
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return responseScanning;
|
|
273
|
+
};
|
|
274
|
+
|