@contrast/assess 1.2.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.
@@ -36,6 +36,7 @@ module.exports = function(core) {
36
36
  require('./install/pug-runtime-escape')(core);
37
37
  require('./install/sql-template-strings')(core);
38
38
  require('./install/unescape')(core);
39
+ require('./install/querystring')(core);
39
40
 
40
41
  propagation.install = function() {
41
42
  callChildComponentMethodsSync(propagation, 'install');
@@ -0,0 +1,30 @@
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 { callChildComponentMethodsSync } = require('@contrast/common');
19
+
20
+ module.exports = function(core) {
21
+ const querystringInstrumentation = core.assess.dataflow.propagation.querystringInstrumentation = {
22
+ install() {
23
+ callChildComponentMethodsSync(querystringInstrumentation, 'install');
24
+ }
25
+ };
26
+
27
+ require('./parse')(core);
28
+
29
+ return querystringInstrumentation;
30
+ };
@@ -0,0 +1,124 @@
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
+ const util = require('util');
18
+ const querystring = require('querystring');
19
+ const { patchType } = require('../../common');
20
+ const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
21
+
22
+ module.exports = function(core) {
23
+ const {
24
+ scopes: { sources, instrumentation },
25
+ patcher,
26
+ depHooks,
27
+ assess: {
28
+ dataflow: { tracker, eventFactory: { createPropagationEvent } }
29
+ }
30
+ } = core;
31
+
32
+ function getUnescapeWrapper(data, unescape) {
33
+ const input = data.args[0];
34
+ const { trackingData } = data;
35
+
36
+ function unescapeWrapper(part) {
37
+ let result = unescape(part);
38
+ const start = input.indexOf(part, data.idx);
39
+ const tagRanges = createSubsetTags(trackingData.tags, start, result.length - 1);
40
+
41
+ if (!tagRanges) return result;
42
+
43
+ const resultInfo = tracker.getData(result);
44
+ const event = createPropagationEvent({
45
+ name: data.name,
46
+ history: [trackingData],
47
+ object: {
48
+ value: part,
49
+ isTracked: true,
50
+ },
51
+ args: data.origArgs.map((arg) => {
52
+ const argInfo = tracker.getData(arg);
53
+ return {
54
+ value: argInfo ? argInfo.value : util.inspect(arg),
55
+ isTracked: !!argInfo
56
+ };
57
+ }),
58
+ result: {
59
+ value: result,
60
+ isTracked: !!resultInfo
61
+ },
62
+ tags: tagRanges,
63
+ stacktraceOpts: {
64
+ constructorOpt: data.hooked,
65
+ prependFrames: [data.orig]
66
+ },
67
+ source: 'P',
68
+ target: 'R'
69
+ });
70
+
71
+ if (event) {
72
+ if (resultInfo) {
73
+ event.tags = createAppendTags(event.tags, resultInfo.tags, 0);
74
+ Object.assign(resultInfo, event);
75
+ }
76
+ if (event.tags['url-encoded']) {
77
+ delete event.tags['url-encoded'];
78
+ event.removedTags = ['url-encoded'];
79
+ }
80
+ const { extern } = resultInfo || tracker.track(result, event);
81
+ if (extern) {
82
+ result = extern;
83
+ }
84
+
85
+ }
86
+ data.idx = start + part.length;
87
+ return result;
88
+ }
89
+ return unescapeWrapper;
90
+ }
91
+
92
+ return core.assess.dataflow.propagation.querystringInstrumentation.parse = {
93
+ install() {
94
+ depHooks.resolve({ name: 'querystring' }, (module) => {
95
+ ['parse', 'decode'].forEach((method) => {
96
+ patcher.patch(module, method, {
97
+ name: `querystring.${method}`,
98
+ patchType,
99
+ pre(data) {
100
+ if (!sources.getStore()?.assess || instrumentation.isLocked()) return;
101
+ const input = data.args[0];
102
+ if (!input) {
103
+ return;
104
+ }
105
+ const trackingData = tracker.getData(input);
106
+ if (!trackingData) {
107
+ return;
108
+ }
109
+
110
+ data.idx = 0;
111
+ data.origArgs = data.args;
112
+ data.trackingData = trackingData;
113
+
114
+ data.args[3] = {
115
+ ...data.args[3],
116
+ decodeURIComponent: getUnescapeWrapper(data, data.args?.[3]?.decodeURIComponent || querystring.unescape)
117
+ };
118
+ }
119
+ });
120
+ });
121
+ });
122
+ }
123
+ };
124
+ };
@@ -28,12 +28,13 @@ module.exports = function(core) {
28
28
  };
29
29
 
30
30
  require('./concat')(core);
31
+ require('./format-methods')(core);
32
+ require('./html-methods')(core);
33
+ require('./match')(core);
31
34
  require('./replace')(core);
35
+ require('./split')(core);
32
36
  require('./substring')(core);
33
37
  require('./trim')(core);
34
- require('./html-methods')(core);
35
- require('./format-methods')(core);
36
- require('./split')(core);
37
38
 
38
39
  return stringInstrumentation;
39
40
  };
@@ -0,0 +1,122 @@
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
+ const { join } = require('@contrast/common');
18
+ const { patchType } = require('../../common');
19
+ const { createSubsetTags } = require('../../../tag-utils');
20
+
21
+ module.exports = function(core) {
22
+ const {
23
+ scopes: { sources, instrumentation },
24
+ patcher,
25
+ assess: {
26
+ dataflow: { tracker, eventFactory: { createPropagationEvent } }
27
+ }
28
+ } = core;
29
+
30
+ function getPropagationEvent(data, res, objInfo, start) {
31
+ const { name, args, result, hooked, orig } = data;
32
+ const tags = createSubsetTags(objInfo.tags, start, res.length - 1);
33
+ if (!tags) return;
34
+
35
+ return createPropagationEvent({
36
+ name,
37
+ history: [objInfo],
38
+ object: {
39
+ value: objInfo.value,
40
+ isTracked: true,
41
+ },
42
+ args: args.map((arg) => {
43
+ const argInfo = tracker.getData(arg);
44
+ return {
45
+ value: argInfo ? argInfo.value : arg.toString(),
46
+ isTracked: !!argInfo
47
+ };
48
+ }),
49
+ tags,
50
+ result: {
51
+ value: join(result),
52
+ isTracked: false
53
+ },
54
+ stacktraceOpts: {
55
+ constructorOpt: hooked,
56
+ prependFrames: [orig]
57
+ },
58
+ source: 'O',
59
+ target: 'R'
60
+ });
61
+ }
62
+
63
+ return core.assess.dataflow.propagation.stringInstrumentation.match = {
64
+ install() {
65
+ const name = 'String.prototype.match';
66
+
67
+ patcher.patch(String.prototype, 'match', {
68
+ name,
69
+ patchType,
70
+ post(data) {
71
+ const { args, obj, result } = data;
72
+ if (
73
+ !obj ||
74
+ !result ||
75
+ args.length === 0 ||
76
+ result.length === 0 ||
77
+ !sources.getStore() ||
78
+ typeof obj !== 'string' ||
79
+ instrumentation.isLocked() ||
80
+ (args.length === 1 && args[0] == null)
81
+ ) return;
82
+
83
+ const objInfo = tracker.getData(obj);
84
+ if (!objInfo) return;
85
+
86
+ let idx = 0;
87
+ const hasCaptureGroups = 'groups' in result;
88
+ for (let i = 0; i < result.length; i++) {
89
+ const res = result[i];
90
+ if (!res) continue;
91
+ const start = obj.indexOf(res, idx);
92
+ idx += hasCaptureGroups ? 0 : res.length;
93
+ const event = getPropagationEvent(data, res, objInfo, start);
94
+ if (event) {
95
+ const { extern } = tracker.track(res, event);
96
+ if (extern) {
97
+ data.result[i] = extern;
98
+ }
99
+ }
100
+ }
101
+ if (hasCaptureGroups && result.groups) {
102
+ Object.keys(result.groups).forEach((key) => {
103
+ const res = result.groups[key];
104
+ if (!res) return;
105
+ const start = obj.indexOf(res);
106
+ const event = getPropagationEvent(data, res, objInfo, start);
107
+ if (event) {
108
+ const { extern } = tracker.track(res, event);
109
+ if (extern) {
110
+ data.result.groups[key] = extern;
111
+ }
112
+ }
113
+ });
114
+ }
115
+ },
116
+ });
117
+ },
118
+ uninstall() {
119
+ String.prototype.match = patcher.unwrap(String.prototype.match);
120
+ },
121
+ };
122
+ };
@@ -106,3 +106,4 @@ module.exports = function(core) {
106
106
  },
107
107
  };
108
108
  };
109
+
@@ -17,6 +17,7 @@
17
17
 
18
18
  module.exports = function(core) {
19
19
  const signaturesMap = core.assess.dataflow.signatures = new Map([
20
+ ...require('./mssql.js'),
20
21
  [
21
22
  'Url.prototype.parse',
22
23
  {
@@ -932,6 +933,16 @@ module.exports = function(core) {
932
933
  target: 'R'
933
934
  }
934
935
  ],
936
+ [
937
+ 'String.prototype.match',
938
+ {
939
+ moduleName: 'String',
940
+ methodName: 'prototype.match',
941
+ isModule: false,
942
+ source: 'O',
943
+ target: 'R'
944
+ }
945
+ ],
935
946
  [
936
947
  'sqlite3.Database.prototype.all',
937
948
  {
@@ -986,33 +997,6 @@ module.exports = function(core) {
986
997
  isModule: true
987
998
  }
988
999
  ],
989
- [
990
- 'mssql/lib/base/prepared-statement.prototype.prepare',
991
- {
992
- moduleName: 'mssql',
993
- version: '>=6.4.0',
994
- methodName: 'PreparedStatement.prototype.prepare',
995
- isModule: true
996
- }
997
- ],
998
- [
999
- 'mssql/lib/base/request.prototype.batch',
1000
- {
1001
- moduleName: 'mssql',
1002
- version: '>=6.4.0',
1003
- methodName: 'Request.prototype.batch',
1004
- isModule: true
1005
- }
1006
- ],
1007
- [
1008
- 'mssql/lib/base/request.prototype.query',
1009
- {
1010
- moduleName: 'mssql',
1011
- version: '>=6.4.0',
1012
- methodName: 'Request.prototype.query',
1013
- isModule: true
1014
- }
1015
- ],
1016
1000
  [
1017
1001
  'path.format',
1018
1002
  {
@@ -0,0 +1,49 @@
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
+ [
20
+ 'mssql/lib/base/prepared-statement.prototype.prepare',
21
+ {
22
+ moduleName: 'mssql',
23
+ version: '>=6.4.0',
24
+ filename: 'lib/base/prepared-statement.js',
25
+ methodName: 'PreparedStatement.prototype.prepare',
26
+ isModule: true,
27
+ },
28
+ ],
29
+ [
30
+ 'mssql/lib/base/request.prototype.batch',
31
+ {
32
+ moduleName: 'mssql',
33
+ version: '>=6.4.0',
34
+ filename: 'lib/base/request.js',
35
+ methodName: 'Request.prototype.batch',
36
+ isModule: true,
37
+ },
38
+ ],
39
+ [
40
+ 'mssql/lib/base/request.prototype.query',
41
+ {
42
+ moduleName: 'mssql',
43
+ version: '>=6.4.0',
44
+ filename: 'lib/base/request.js',
45
+ methodName: 'Request.prototype.query',
46
+ isModule: true,
47
+ },
48
+ ],
49
+ ];
@@ -32,9 +32,10 @@ module.exports = function (core) {
32
32
  },
33
33
  };
34
34
 
35
+ require('./install/fastify')(core);
35
36
  require('./install/http')(core);
37
+ require('./install/mssql')(core);
36
38
  require('./install/postgres')(core);
37
- require('./install/fastify')(core);
38
39
 
39
40
  sinks.install = function() {
40
41
  callChildComponentMethodsSync(core.assess.dataflow.sinks, 'install');
@@ -70,7 +70,10 @@ module.exports = function (core) {
70
70
  if (isVulnerable(requiredTag, safeTags, strInfo.tags)) {
71
71
  const event = createSinkEvent({
72
72
  name: 'fastify.reply.redirect',
73
- object: `[${createModuleLabel('fastify', version)}].Reply`,
73
+ object: {
74
+ value: `[${createModuleLabel('fastify', version)}].Reply`,
75
+ isTracked: false,
76
+ },
74
77
  history: [strInfo],
75
78
  args: [{
76
79
  value: strInfo.value,
@@ -88,7 +91,7 @@ module.exports = function (core) {
88
91
  });
89
92
 
90
93
  reportFindings({
91
- ruleId: 'unvalidated-redirect',
94
+ ruleId: 'unvalidated-redirect', // add Rule.UNVALIDATED_REDIRECT
92
95
  metadata: event,
93
96
  });
94
97
  }
@@ -0,0 +1,123 @@
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 { Rule, isString } = require('@contrast/common');
19
+ const { createModuleLabel } = require('../../propagation/common');
20
+ const { patchType } = require('../common');
21
+
22
+ const SAFE_TAGS = [
23
+ 'sql-encoded',
24
+ 'limited-chars',
25
+ 'custom-validated',
26
+ 'custom-encoded',
27
+ ];
28
+
29
+ module.exports = function (core) {
30
+ const {
31
+ depHooks,
32
+ patcher,
33
+ scopes: { sources },
34
+ assess: {
35
+ dataflow: {
36
+ tracker,
37
+ sinks: { isVulnerable, reportFindings },
38
+ eventFactory: { createSinkEvent },
39
+ },
40
+ },
41
+ } = core;
42
+
43
+ const pre = (name, obj, version) => (data) => {
44
+ const store = sources.getStore()?.assess;
45
+ if (!store || !data.args[0] || !isString(data.args[0])) return;
46
+
47
+ const strInfo = tracker.getData(data.args[0]);
48
+ if (!strInfo || !isVulnerable('untrusted', SAFE_TAGS, strInfo.tags)) {
49
+ return;
50
+ }
51
+
52
+ const event = createSinkEvent({
53
+ name,
54
+ history: [strInfo],
55
+ object: {
56
+ value: `[${createModuleLabel('mssql', version)}].${obj}`,
57
+ isTracked: false,
58
+ },
59
+ args: [
60
+ {
61
+ value: strInfo.value,
62
+ isTracked: true,
63
+ },
64
+ ],
65
+ tags: strInfo.tags,
66
+ source: 'P0',
67
+ stacktraceOpts: {
68
+ contructorOpt: data.hooked,
69
+ },
70
+ });
71
+
72
+ reportFindings({
73
+ ruleId: Rule.SQL_INJECTION,
74
+ metadata: event,
75
+ });
76
+ };
77
+
78
+ core.assess.dataflow.sinks.mssql = {
79
+ install() {
80
+ depHooks.resolve(
81
+ { name: 'mssql', file: 'lib/base/prepared-statement.js' },
82
+ (PreparedStatement, version) => {
83
+ patcher.patch(PreparedStatement.prototype, 'prepare', {
84
+ name: 'PreparedStatement.prototype.prepare',
85
+ patchType,
86
+ pre: pre(
87
+ 'mssql/lib/base/prepared-statement.prototype.prepare',
88
+ 'PreparedStatement',
89
+ version,
90
+ ),
91
+ });
92
+ },
93
+ );
94
+
95
+ depHooks.resolve(
96
+ { name: 'mssql', file: 'lib/base/request.js' },
97
+ (Request, version) => {
98
+ patcher.patch(Request.prototype, 'batch', {
99
+ name: 'Request.prototype.batch',
100
+ patchType,
101
+ pre: pre(
102
+ 'mssql/lib/base/request.prototype.batch',
103
+ 'Request',
104
+ version,
105
+ ),
106
+ });
107
+
108
+ patcher.patch(Request.prototype, 'query', {
109
+ name: 'Request.prototype.query',
110
+ patchType,
111
+ pre: pre(
112
+ 'mssql/lib/base/request.prototype.query',
113
+ 'Request',
114
+ version,
115
+ ),
116
+ });
117
+ },
118
+ );
119
+ },
120
+ };
121
+
122
+ return core.assess.dataflow.sinks.mssql;
123
+ };
@@ -15,9 +15,9 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { isString } = require('@contrast/common');
19
- const { patchType } = require('../../common');
20
- const util = require('util');
18
+ const { Rule, isString } = require('@contrast/common');
19
+ const { createModuleLabel } = require('../../propagation/common');
20
+ const { patchType } = require('../common');
21
21
 
22
22
  module.exports = function (core) {
23
23
  const {
@@ -43,7 +43,7 @@ module.exports = function (core) {
43
43
  ];
44
44
  const requiredTag = 'untrusted';
45
45
 
46
- const preHook = (methodSignature) => (data) => {
46
+ const preHook = (methodSignature, version, mod, obj) => (data) => {
47
47
  const assessStore = sources.getStore()?.assess;
48
48
  if (!assessStore) return;
49
49
 
@@ -57,16 +57,16 @@ module.exports = function (core) {
57
57
  const event = createSinkEvent({
58
58
  name: methodSignature,
59
59
  history: [strInfo],
60
+ object: {
61
+ value: `[${createModuleLabel(mod, version)}].${obj}`,
62
+ isTracked: false,
63
+ },
60
64
  args: [
61
65
  {
62
- value: util.inspect(data.args[0]),
66
+ value: strInfo.value,
63
67
  isTracked: true,
64
68
  },
65
69
  ],
66
- result: {
67
- value: data.result,
68
- isTracked: false,
69
- },
70
70
  tags: strInfo.tags,
71
71
  source: 'P0',
72
72
  stacktraceOpts: {
@@ -75,7 +75,7 @@ module.exports = function (core) {
75
75
  });
76
76
 
77
77
  reportFindings({
78
- ruleId: 'sql-injection',
78
+ ruleId: Rule.SQL_INJECTION,
79
79
  metadata: event,
80
80
  });
81
81
  }
@@ -83,43 +83,50 @@ module.exports = function (core) {
83
83
 
84
84
  postgres.install = function () {
85
85
  const pgClientQueryPatchName = 'pg.Client.prototype.query';
86
- depHooks.resolve({ name: 'pg', file: 'lib/client.js' }, client => {
87
- patcher.patch(client.prototype, 'query', {
88
- name: pgClientQueryPatchName,
89
- patchType,
90
- pre: preHook('pg/lib/client.prototype.query'),
91
- });
92
- });
86
+ depHooks.resolve(
87
+ { name: 'pg', file: 'lib/client.js' },
88
+ (client, version) => {
89
+ patcher.patch(client.prototype, 'query', {
90
+ name: pgClientQueryPatchName,
91
+ patchType,
92
+ pre: preHook('pg/lib/client.prototype.query', version, 'pg', 'Client'),
93
+ });
94
+ },
95
+ );
93
96
 
94
97
  const pgNativeClientQueryPatchName = 'pg.native.Client.prototype.query';
95
- depHooks.resolve({ name: 'pg', file: 'lib/native/client.js' }, (client) => {
96
- patcher.patch(client.prototype, 'query', {
97
- name: pgNativeClientQueryPatchName,
98
- patchType,
99
- pre: preHook('pg/lib/native/client.prototype.query'),
100
- });
101
- });
98
+ depHooks.resolve(
99
+ { name: 'pg', file: 'lib/native/client.js' },
100
+ (client, version) => {
101
+ patcher.patch(client.prototype, 'query', {
102
+ name: pgNativeClientQueryPatchName,
103
+ patchType,
104
+ pre: preHook('pg/lib/native/client.prototype.query', version, 'pg', 'native.Client'),
105
+ });
106
+ },
107
+ );
102
108
 
103
109
  const pgClientPatchName = `${patchType}:${pgClientQueryPatchName}.query`;
104
110
  const pgNativeClientPatchName = `${patchType}:${pgNativeClientQueryPatchName}.query`;
105
- depHooks.resolve({ name: 'pg-pool' }, (pool) => {
111
+ depHooks.resolve({ name: 'pg-pool' }, (pool, version) => {
106
112
  const name = 'pg-pool.Pool.prototype.query';
107
113
  patcher.patch(pool.prototype, 'query', {
108
114
  name,
109
115
  patchType,
110
116
  pre: (data) => {
111
117
  const funcKeys = patcher.hooks.get(
112
- data.obj.Client?.prototype?.query
118
+ data.obj.Client?.prototype?.query,
113
119
  )?.funcKeys;
114
120
 
115
121
  if (
116
122
  funcKeys &&
117
- (funcKeys.has(pgClientPatchName) || funcKeys.has(pgNativeClientPatchName))
123
+ (funcKeys.has(pgClientPatchName) ||
124
+ funcKeys.has(pgNativeClientPatchName))
118
125
  ) {
119
126
  return;
120
127
  }
121
128
 
122
- preHook(name)(data);
129
+ preHook(name, version, 'pg-pool', 'Pool')(data);
123
130
  },
124
131
  });
125
132
  });
@@ -132,43 +132,15 @@ module.exports = function(core) {
132
132
  }
133
133
 
134
134
  function install() {
135
- [{
136
- moduleName: 'http'
137
- },
138
- {
139
- moduleName: 'https'
140
- },
141
- {
142
- moduleName: 'spdy'
143
- },
144
- {
145
- moduleName: 'http2',
146
- patchObjectsProps: [
147
- {
148
- methods: ['createServer', 'createSecureServer'],
149
- patchType,
150
- patchObjects: [
151
- {
152
- name: 'Server.prototype',
153
- methods: ['emit'],
154
- patchType,
155
- around
156
- }
157
- ]
158
- }
159
- ]
160
- }].forEach(({ moduleName, patchObjectsProps }) => {
161
- const patchObjects = patchObjectsProps || [
162
- {
135
+ ['http', 'https', 'spdy', 'http2'].forEach((moduleName) => {
136
+ instrument({
137
+ moduleName,
138
+ patchObjects: [{
163
139
  name: 'Server.prototype',
164
140
  methods: ['emit'],
165
141
  patchType,
166
142
  around
167
- }
168
- ];
169
- instrument({
170
- moduleName,
171
- patchObjects
143
+ }]
172
144
  });
173
145
  });
174
146
  }
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const distringuish = require('@contrast/distringuish');
19
+ const { isString } = require('@contrast/common');
19
20
 
20
21
  module.exports = function tracker(core) {
21
22
  const {
@@ -66,11 +67,35 @@ module.exports = function tracker(core) {
66
67
  return { extern: null };
67
68
  }
68
69
 
69
- const extern = distringuish.externalize(value);
70
- /* c8 ignore next 5 */
71
- if (!extern) {
70
+ let extern = distringuish.externalize(value);
71
+
72
+ if (extern == 2) {
73
+ // Try work-around for some wrong/unknown encoding
74
+ extern = distringuish.externalize(Buffer.from(value).toString());
75
+ }
76
+
77
+ if (!extern || !isString(extern)) {
78
+ let errMsg;
79
+ switch (extern) {
80
+ case 1:
81
+ errMsg = 'zero-length string was passed';
82
+ break;
83
+ case 2:
84
+ errMsg = 'non-two-byte encoded string was passed';
85
+ break;
86
+ case 3:
87
+ errMsg = 'distringuish was unable to convert a MaybeLocal to Local';
88
+ break;
89
+ case 4:
90
+ errMsg = 'distinguish\'s isExternal call returned false';
91
+ break;
92
+ default:
93
+ errMsg = 'unknown error while trying to externalize the string was encountered';
94
+ break;
95
+ }
96
+
72
97
  const err = new Error();
73
- logger.error({ err, value }, 'tracker.track was unable to externalize');
98
+ logger.error({ err, value }, `tracker.track was unable to externalize because ${errMsg}`);
74
99
  return { extern: null };
75
100
  }
76
101
 
@@ -60,6 +60,27 @@ module.exports = function(core) {
60
60
 
61
61
  const patchType = 'response-scanning';
62
62
 
63
+ function writeHookChecks(sourceContext, evaluationContext) {
64
+ // Check only the rules concerning the response body
65
+ handleAutoCompleteMissing(sourceContext, evaluationContext);
66
+ handleCacheControlsMissing(sourceContext, evaluationContext);
67
+ handleParameterPollution(sourceContext, evaluationContext);
68
+ handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
69
+ }
70
+
71
+ function endHookChecks(sourceContext, evaluationContext) {
72
+ // Check all the response scanning rules
73
+ handleAutoCompleteMissing(sourceContext, evaluationContext);
74
+ handleCacheControlsMissing(sourceContext, evaluationContext);
75
+ handleClickJackingControlsMissing(sourceContext, evaluationContext);
76
+ handleParameterPollution(sourceContext, evaluationContext);
77
+ handleCspHeader(sourceContext, evaluationContext);
78
+ handleHstsHeaderMissing(sourceContext, evaluationContext);
79
+ handlePoweredByHeader(sourceContext, evaluationContext);
80
+ handleXContentTypeHeaderMissing(sourceContext, evaluationContext);
81
+ handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
82
+ }
83
+
63
84
  http.install = function() {
64
85
  depHooks.resolve({ name: 'http' }, (http) => {
65
86
  {
@@ -76,11 +97,7 @@ module.exports = function(core) {
76
97
  responseHeaders: parseHeaders(data.result._header),
77
98
  };
78
99
 
79
- // Check only the rules concerning the response body
80
- handleAutoCompleteMissing(sourceContext, evaluationContext);
81
- handleCacheControlsMissing(sourceContext, evaluationContext);
82
- handleParameterPollution(sourceContext, evaluationContext);
83
- handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
100
+ writeHookChecks(sourceContext, evaluationContext);
84
101
  }
85
102
  });
86
103
  }
@@ -98,19 +115,104 @@ module.exports = function(core) {
98
115
  responseHeaders: parseHeaders(data.result._header),
99
116
  };
100
117
 
101
- // Check all the response scanning rules
102
- handleAutoCompleteMissing(sourceContext, evaluationContext);
103
- handleCacheControlsMissing(sourceContext, evaluationContext);
104
- handleClickJackingControlsMissing(sourceContext, evaluationContext);
105
- handleParameterPollution(sourceContext, evaluationContext);
106
- handleCspHeader(sourceContext, evaluationContext);
107
- handleHstsHeaderMissing(sourceContext, evaluationContext);
108
- handlePoweredByHeader(sourceContext, evaluationContext);
109
- handleXContentTypeHeaderMissing(sourceContext, evaluationContext);
110
- handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
118
+ endHookChecks(sourceContext, evaluationContext);
119
+ }
120
+ });
121
+ }
122
+ });
123
+
124
+ depHooks.resolve({ name: 'http2' }, (http2) => {
125
+ // Patching the response object
126
+ {
127
+ const name = 'http2.Http2ServerResponse.prototype.write';
128
+ patcher.patch(http2.Http2ServerResponse.prototype, 'write', {
129
+ name,
130
+ patchType,
131
+ post(data) {
132
+ const sourceContext = sources.getStore()?.assess;
133
+ if (!sourceContext) return;
134
+
135
+ const headersSymbol = Object.getOwnPropertySymbols(data.obj).find(symbol => symbol.toString().includes('headers'));
136
+ const evaluationContext = {
137
+ responseBody: toLowerCase(data.args[0] || ''),
138
+ responseHeaders: data.obj[headersSymbol],
139
+ };
140
+
141
+ writeHookChecks(sourceContext, evaluationContext);
142
+ }
143
+ });
144
+ }
145
+ {
146
+ const name = 'http2.Http2ServerResponse.prototype.end';
147
+ patcher.patch(http2.Http2ServerResponse.prototype, 'end', {
148
+ name,
149
+ patchType,
150
+ post(data) {
151
+ const sourceContext = sources.getStore()?.assess;
152
+ if (!sourceContext) return;
153
+
154
+ const headersSymbol = Object.getOwnPropertySymbols(data.result).find(symbol => symbol.toString().includes('headers'));
155
+ const evaluationContext = {
156
+ responseBody: toLowerCase(data.args[0] || ''),
157
+ responseHeaders: data.result[headersSymbol],
158
+ };
159
+
160
+ endHookChecks(sourceContext, evaluationContext);
111
161
  }
112
162
  });
113
163
  }
164
+
165
+ // patching the stream object
166
+ ['createServer', 'createSecureServer'].forEach((server) => {
167
+ patcher.patch(http2, server, {
168
+ name: `http2.${server}`,
169
+ patchType,
170
+ post(data) {
171
+ // The other option is once again to patch the `emit` method of the server
172
+ // similar to how we patch it for creating sources, but take action only on
173
+ // `stream` events. I chose the currentt approach because the hook will only
174
+ // run on a `stream` event instead of checking and returning on each `request`
175
+ // connect.
176
+ data.result._events = patcher.patch(data.result._events, 'stream', {
177
+ name: 'stream',
178
+ patchType: 'stream-patch',
179
+ pre(data) {
180
+ patcher.patch(data.args[0], 'write', {
181
+ name: 'Http2Stream.write',
182
+ patchType,
183
+ post(data) {
184
+ const sourceContext = sources.getStore()?.assess;
185
+ if (!sourceContext) return;
186
+
187
+ const evaluationContext = {
188
+ responseBody: toLowerCase(data.args[0] || ''),
189
+ responseHeaders: data.obj.sentHeaders,
190
+ };
191
+
192
+ writeHookChecks(sourceContext, evaluationContext);
193
+ }
194
+ });
195
+
196
+ patcher.patch(data.args[0], 'end', {
197
+ name: 'Http2Stream.end',
198
+ patchType,
199
+ post(data) {
200
+ const sourceContext = sources.getStore()?.assess;
201
+ if (!sourceContext) return;
202
+
203
+ const evaluationContext = {
204
+ responseBody: toLowerCase(data.args[0] || ''),
205
+ responseHeaders: data.obj.sentHeaders,
206
+ };
207
+
208
+ endHookChecks(sourceContext, evaluationContext);
209
+ }
210
+ });
211
+ }
212
+ });
213
+ }
214
+ });
215
+ });
114
216
  });
115
217
  };
116
218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  "dependencies": {
16
16
  "@contrast/distringuish": "^4.1.0",
17
17
  "@contrast/scopes": "1.3.0",
18
- "@contrast/common": "1.5.0",
18
+ "@contrast/common": "1.7.0",
19
19
  "parseurl": "^1.3.3"
20
20
  }
21
21
  }