@contrast/protect 1.0.1 → 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.
Files changed (29) hide show
  1. package/lib/error-handlers/index.js +2 -0
  2. package/lib/error-handlers/install/koa2.js +53 -0
  3. package/lib/input-analysis/index.js +2 -0
  4. package/lib/input-analysis/install/co-body.js +51 -0
  5. package/lib/input-analysis/install/cookie-parser.js +48 -0
  6. package/lib/input-analysis/install/formidable.js +53 -0
  7. package/lib/input-analysis/install/koa2.js +137 -0
  8. package/lib/input-analysis/install/multer.js +52 -0
  9. package/lib/input-analysis/install/qs.js +40 -0
  10. package/lib/input-analysis/install/universal-cookie.js +34 -0
  11. package/package.json +4 -3
  12. package/lib/error-handlers/install/fastify3.test.js +0 -142
  13. package/lib/esm-loader.test.mjs +0 -11
  14. package/lib/index.test.js +0 -32
  15. package/lib/input-analysis/handlers.test.js +0 -898
  16. package/lib/input-analysis/index.test.js +0 -28
  17. package/lib/input-analysis/install/fastify3.test.js +0 -71
  18. package/lib/input-analysis/install/http.test.js +0 -315
  19. package/lib/input-tracing/handlers/index.test.js +0 -395
  20. package/lib/input-tracing/install/child-process.test.js +0 -112
  21. package/lib/input-tracing/install/fs.test.js +0 -118
  22. package/lib/input-tracing/install/mysql.test.js +0 -108
  23. package/lib/input-tracing/install/postgres.test.js +0 -125
  24. package/lib/input-tracing/install/sequelize.test.js +0 -79
  25. package/lib/input-tracing/install/sqlite3.test.js +0 -88
  26. package/lib/make-response-blocker.test.js +0 -88
  27. package/lib/make-source-context.test.js +0 -298
  28. package/lib/throw-security-exception.test.js +0 -50
  29. package/lib/utils.test.js +0 -40
@@ -4,9 +4,11 @@ module.exports = function(core) {
4
4
  const errorHandlers = core.protect.errorHandlers = {};
5
5
 
6
6
  require('./install/fastify3')(core);
7
+ require('./install/koa2')(core);
7
8
 
8
9
  errorHandlers.install = function() {
9
10
  errorHandlers.fastify3ErrorHandler.install();
11
+ errorHandlers.koa2ErrorHandler.install();
10
12
  };
11
13
 
12
14
  return errorHandlers;
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const SecurityException = require('../../security-exception');
4
+ const { patchType } = require('../constants');
5
+
6
+
7
+ module.exports = function(core) {
8
+ const {
9
+ logger,
10
+ depHooks,
11
+ patcher,
12
+ scopes: { sources },
13
+ protect,
14
+ } = core;
15
+
16
+ const koa2ErrorHandler = protect.errorHandlers.koa2ErrorHandler = {};
17
+
18
+ koa2ErrorHandler.install = function () {
19
+ depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
20
+ patcher.patch(Koa.prototype, 'handleRequest', {
21
+ name: 'Koa.Application',
22
+ patchType,
23
+ pre(data) {
24
+ const [ctx] = data.args;
25
+ patcher.patch(ctx, 'onerror', {
26
+ name: 'koa.ctx.onerror',
27
+ patchType,
28
+ around(org, data) {
29
+ const [err] = data.args;
30
+ const sourceContext = sources.getStore()?.protect;
31
+ const isSecurityException = SecurityException.isSecurityException(err);
32
+
33
+ if (isSecurityException && sourceContext) {
34
+ data.obj.body = '';
35
+ const blockInfo = sourceContext.findings.securityException;
36
+ sourceContext.block(...blockInfo);
37
+ return;
38
+ }
39
+
40
+ if (!sourceContext && isSecurityException) {
41
+ logger.info('source context not found; unable to handle response');
42
+ }
43
+
44
+ org();
45
+ }
46
+ });
47
+ }
48
+ });
49
+ });
50
+ };
51
+
52
+ return koa2ErrorHandler;
53
+ };
@@ -6,10 +6,12 @@ module.exports = function(core) {
6
6
  require('./handlers')(core);
7
7
  require('./install/http')(core);
8
8
  require('./install/fastify3')(core);
9
+ require('./install/koa2')(core);
9
10
 
10
11
  inputAnalysis.install = function() {
11
12
  inputAnalysis.httpInstrumentation.install();
12
13
  inputAnalysis.fastifyInstrumentation.install();
14
+ inputAnalysis.koaInstrumentation.install();
13
15
  };
14
16
 
15
17
  return inputAnalysis;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ async function postHook(data) {
13
+ const { args: [, opts], result } = data;
14
+ if (result) {
15
+ const sourceContext = sources.getStore()?.protect;
16
+ if (!sourceContext) {
17
+ logger.debug('source context not available in `co-body` hook');
18
+ } else {
19
+ result.then((resolved) => {
20
+ const parsedBody = opts?.returnRawBody ? resolved.parsed : resolved;
21
+ sourceContext.parsedBody = parsedBody;
22
+ inputAnalysis.handleParsedBody(sourceContext, parsedBody);
23
+ });
24
+ }
25
+ }
26
+ }
27
+
28
+ return {
29
+ // Patch lower level parser - `co-body` used by `koa-body` and `koa-bodyparser`
30
+ install() {
31
+ depHooks.resolve({ name: 'co-body' }, (coBody) => {
32
+ coBody = patcher.patch(coBody, {
33
+ name: 'co-body',
34
+ patchType: 'framework-patch',
35
+ post: postHook
36
+ });
37
+
38
+ ['json', 'form', 'text'].forEach((property) => {
39
+ patcher.patch(coBody, property, {
40
+ name: `co-body.${property}`,
41
+ patchType: 'framework-patch',
42
+ post: postHook
43
+ });
44
+ });
45
+
46
+ return coBody;
47
+ }
48
+ );
49
+ }
50
+ };
51
+ };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ return {
13
+ // Patch `cookie-parser` package
14
+ install() {
15
+ depHooks.resolve({ name: 'cookie-parser' }, (cookieParser) => patcher.patch(cookieParser, {
16
+ name: 'cookie-parser',
17
+ patchType: 'framework-patch',
18
+ post(data) {
19
+ data.result = patcher.patch(data.result, {
20
+ name: 'cookie-parser',
21
+ patchType: 'framework-patch',
22
+ pre(data) {
23
+ const [req, , origNext] = data.args;
24
+
25
+ async function contrastNext() {
26
+ const sourceContext = sources.getStore()?.protect;
27
+
28
+ if (!sourceContext) {
29
+ logger.debug('source context not available in `cookie-parser` hook');
30
+ } else {
31
+ if (req.cookies) {
32
+ sourceContext.parsedCookies = req.cookies;
33
+ inputAnalysis.handleCookies(sourceContext, req.cookies);
34
+ }
35
+ }
36
+
37
+ await origNext();
38
+ }
39
+
40
+ data.args[2] = contrastNext;
41
+ }
42
+ });
43
+ }
44
+ })
45
+ );
46
+ }
47
+ };
48
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ return {
13
+ // Patch `formidable`
14
+ install() {
15
+ depHooks.resolve({ name: 'formidable' }, (formidable) => {
16
+ formidable.IncomingForm.prototype.parse = patcher.patch(formidable.IncomingForm.prototype.parse, {
17
+ name: 'Formidable.IncomingForm.prototype.parse',
18
+ patchType: 'framework-patch',
19
+ pre(data) {
20
+ const origCb = data.args[1];
21
+
22
+ function hookedCb(...cbArgs) {
23
+ const sourceContext = sources.getStore()?.protect;
24
+ const [, fields, files] = cbArgs;
25
+
26
+ if (!sourceContext) {
27
+ logger.debug('source context not available in `formidable` hook');
28
+ } else {
29
+ if (fields) {
30
+ sourceContext.parsedBody = fields;
31
+ inputAnalysis.handleParsedBody(sourceContext, fields);
32
+ }
33
+ if (files) {
34
+ logger.debug('Check for vulnerable filename upload nyi');
35
+ // CHECK FILENAME - NYI
36
+ }
37
+ }
38
+
39
+ if (origCb && typeof origCb === 'function') {
40
+ // Should we explicitly run in the current source context?
41
+ origCb.apply(this, cbArgs);
42
+ }
43
+ }
44
+
45
+ data.args[1] = hookedCb;
46
+ }
47
+ });
48
+
49
+ return formidable;
50
+ });
51
+ }
52
+ };
53
+ };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Function that exports an install method to patch Fastify framework with our instrumentation
5
+ * @param {Object} core - the core Contrast object in v5
6
+ * @return {Object} object with install method and the other relative functions exported for testing purposes
7
+ */
8
+ module.exports = (core) => {
9
+ const {
10
+ depHooks,
11
+ patcher,
12
+ logger,
13
+ scopes: { sources },
14
+ protect: { inputAnalysis },
15
+ } = core;
16
+
17
+ /**
18
+ * registers a depHook for koa module instrumentation
19
+ */
20
+ function install() {
21
+ depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
22
+ const coBodyPatch = require('./co-body.js')(core);
23
+ const multerPatch = require('./multer')(core);
24
+ const formidablePatch = require('./formidable')(core);
25
+ const qsPatch = require('./qs')(core);
26
+ const cookieParserPatch = require('./cookie-parser')(core);
27
+ const universalCookiePatch = require('./universal-cookie')(core);
28
+
29
+
30
+ function contrastStartMiddleware(ctx, next) {
31
+ if (ctx.query && Object.keys(ctx.query).length) {
32
+ const sourceContext = sources.getStore()?.protect;
33
+
34
+ if (!sourceContext) {
35
+ logger.debug('source context not available in `qs` hook');
36
+ } else if (!('parsedQuery' in sourceContext)) {
37
+ sourceContext.parsedQuery = ctx.query;
38
+ inputAnalysis.handleQueryParams(sourceContext, ctx.query);
39
+ }
40
+
41
+ }
42
+ return next();
43
+ }
44
+
45
+ // mark these middleware as ours
46
+ contrastStartMiddleware._isContrastStartMiddleware = true;
47
+
48
+ patcher.patch(Koa.prototype, 'use', {
49
+ name: 'Koa.Application',
50
+ patchType: 'framework-patch',
51
+ pre({ obj: app }) {
52
+ // if not already inserted, insert the initial middleware.
53
+ if (
54
+ app.middleware &&
55
+ (!app.middleware[0] || !app.middleware[0]._isContrastStartMiddleware)
56
+ ) {
57
+ app.middleware.splice(0, 0, contrastStartMiddleware);
58
+ }
59
+ }
60
+ });
61
+
62
+ // Patch `koa-router` and `@koa/router` to handle parsed params
63
+ ['koa-router', '@koa/router'].forEach(router => {
64
+ depHooks.resolve(
65
+ { name: router, file: 'lib/layer.js' },
66
+ (layer) => {
67
+ layer.prototype = patcher.patch(layer.prototype, 'params', {
68
+ name: `[${router}].layer.prototype`,
69
+ patchType: 'framework-patch',
70
+ post({ result }) {
71
+ const sourceContext = sources.getStore()?.protect;
72
+
73
+ if (!sourceContext) {
74
+ logger.debug(`source context not available in \`[${router}].layer\` hook`);
75
+ } else {
76
+ if (Object.keys(result).length) {
77
+ sourceContext.parsedParams = result;
78
+ inputAnalysis.handleUrlParams(sourceContext, result);
79
+ }
80
+ }
81
+ }
82
+ });
83
+
84
+ return layer;
85
+ });
86
+ });
87
+
88
+ // Patch `koa-cookie`
89
+ depHooks.resolve({ name: 'koa-cookie' }, (koaCookie) => {
90
+ const { default: cookieParser } = koaCookie;
91
+ koaCookie.default = patcher.patch(cookieParser, {
92
+ name: 'koa-cookie',
93
+ patchType: 'framework-patch',
94
+ post(data) {
95
+ data.result = patcher.patch(data.result, {
96
+ name: 'koa-cookie',
97
+ patchType: 'framework-patch',
98
+ pre(data) {
99
+ const [ctx, origNext] = data.args;
100
+
101
+ async function contrastNext() {
102
+ const sourceContext = sources.getStore()?.protect;
103
+
104
+ if (!sourceContext) {
105
+ logger.debug('source context not available in `koa-cookie` hook');
106
+ } else {
107
+ if (ctx.cookie) {
108
+ sourceContext.parsedCookies = ctx.cookie;
109
+ inputAnalysis.handleCookies(sourceContext, ctx.cookie);
110
+ }
111
+ }
112
+
113
+ await origNext();
114
+ }
115
+
116
+ data.args[1] = contrastNext;
117
+ }
118
+ });
119
+ }
120
+ });
121
+ });
122
+
123
+ coBodyPatch.install();
124
+ multerPatch.install();
125
+ formidablePatch.install();
126
+ qsPatch.install();
127
+ cookieParserPatch.install();
128
+ universalCookiePatch.install();
129
+ });
130
+ }
131
+
132
+ const koaInstrumentation = inputAnalysis.koaInstrumentation = {
133
+ install
134
+ };
135
+
136
+ return koaInstrumentation;
137
+ };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ return {
13
+ // Patch `multer`
14
+ install() {
15
+ depHooks.resolve({ name: 'multer', file: 'lib/make-middleware.js' }, (multerMakeMiddleware) => patcher.patch(multerMakeMiddleware, {
16
+ name: 'multer.make-middleware',
17
+ patchType: 'framework-patch',
18
+ post(data) {
19
+ data.result = patcher.patch(data.result, {
20
+ name: 'multerMiddleware',
21
+ patchType: 'framework-patch',
22
+ pre(data) {
23
+ const [req, , origNext] = data.args;
24
+
25
+ async function contrastNext() {
26
+
27
+ const sourceContext = sources.getStore()?.protect;
28
+
29
+ if (!sourceContext) {
30
+ logger.debug('source context not available in `multer` hook');
31
+ } else {
32
+ if (req.body) {
33
+ sourceContext.parsedBody = req.body;
34
+ inputAnalysis.handleParsedBody(sourceContext, req.body);
35
+ }
36
+ if (req.file || req.files) {
37
+ logger.debug('Check for vulnerable filename upload nyi');
38
+ // CHECK FILENAME - NYI
39
+ }
40
+ }
41
+
42
+ await origNext();
43
+ }
44
+
45
+ data.args[2] = contrastNext;
46
+ }
47
+ });
48
+ }
49
+ }));
50
+ }
51
+ };
52
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ return {
13
+ // Patch `qs`
14
+ install() {
15
+ depHooks.resolve({ name: 'qs' },
16
+ (qs) => patcher.patch(qs, 'parse', {
17
+ name: 'qs',
18
+ patchType: 'framework-patch',
19
+ post({ args, result }) {
20
+ if (result && Object.keys(result).length) {
21
+ const sourceContext = sources.getStore()?.protect;
22
+
23
+ if (!sourceContext) {
24
+ logger.debug('source context not available in `qs` hook');
25
+
26
+ // We need to run analysis for the `qs` result only when it's used as a query parser.
27
+ // `qs` is used also for parsing bodies, but these cases we handle individually with
28
+ // the respective library that's using it (e.g. `formidable`, `co-body`) because in
29
+ // some cases its use is optional and we cannot rely on it.
30
+ } else if (sourceContext.reqData?.queries === args[0]) {
31
+ sourceContext.parsedQuery = result;
32
+ inputAnalysis.handleQueryParams(sourceContext, result);
33
+ }
34
+ }
35
+ }
36
+ })
37
+ );
38
+ }
39
+ };
40
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ module.exports = (core) => {
4
+ const {
5
+ depHooks,
6
+ patcher,
7
+ logger,
8
+ scopes: { sources },
9
+ protect: { inputAnalysis },
10
+ } = core;
11
+
12
+ return {
13
+ // Patch `universal-cookie` package
14
+ install() {
15
+ depHooks.resolve({ name: 'universal-cookie', file: 'cjs/utils.js' }, (uCookieUtils) => patcher.patch(uCookieUtils, 'parseCookies', {
16
+ name: 'universal-cookie.utils',
17
+ patchType: 'framework-patch',
18
+ post({ result }) {
19
+ if (result && Object.keys(result).length) {
20
+ const sourceContext = sources.getStore()?.protect;
21
+
22
+ if (!sourceContext) {
23
+ logger.debug('source context not available in `universal-cookie` hook');
24
+ } else {
25
+ sourceContext.parsedCookies = result;
26
+ inputAnalysis.handleCookies(sourceContext, result);
27
+ }
28
+ }
29
+ }
30
+ })
31
+ );
32
+ }
33
+ };
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.0.1",
3
+ "version": "1.2.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)",
@@ -24,8 +24,9 @@
24
24
  "@babel/types": "^7.16.8",
25
25
  "@contrast/agent-lib": "^4.2.0",
26
26
  "@contrast/common": "1.0.0",
27
- "@contrast/core": "1.0.0",
28
- "@contrast/esm-hooks": "1.0.0",
27
+ "@contrast/core": "1.1.0",
28
+ "@contrast/scopes": "1.0.0",
29
+ "@contrast/esm-hooks": "1.1.0",
29
30
  "async-hook-domain": "^2.0.4",
30
31
  "builtin-modules": "^3.2.0"
31
32
  }
@@ -1,142 +0,0 @@
1
- 'use strict';
2
-
3
- const sinon = require('sinon');
4
- const { expect } = require('chai');
5
- const securityException = require('../../security-exception');
6
- const mocks = require('../../../../test/mocks');
7
-
8
- describe('protect error-handlers: fastify3', function() {
9
- let core;
10
- let errorHandler;
11
- let fastify;
12
- let server;
13
- let reply;
14
-
15
- beforeEach(function() {
16
- core = mocks.core();
17
- core.config = mocks.config();
18
- core.logger = mocks.logger();
19
- core.scopes = mocks.scopes();
20
- core.protect = mocks.protect();
21
- core.depHooks = mocks.depHooks();
22
- core.patcher = require('@contrast/patcher')(core);
23
-
24
- server = {
25
- setErrorHandler: sinon.stub(),
26
- errorHandler: sinon.stub()
27
- };
28
- fastify = function() {
29
- return server;
30
- };
31
- core.depHooks.resolve.callsFake((desc, cb) => {
32
- fastify = cb(fastify);
33
- });
34
- reply = {
35
- log: {
36
- info: sinon.stub(),
37
- error: sinon.stub()
38
- },
39
- send: sinon.stub(),
40
- statusCode: 500,
41
- };
42
-
43
- errorHandler = require('./fastify3')(core);
44
- sinon.spy(errorHandler, 'defaultErrorHandler');
45
- errorHandler.install();
46
-
47
- fastify();
48
- });
49
-
50
- describe('instrumentation adds custom handler', function() {
51
- it('patches fastify', function() {
52
- expect(server.setErrorHandler).to.have.been.calledWith(errorHandler.handler);
53
- });
54
- });
55
-
56
- describe('defaultErrorHandler()', function() {
57
- let request;
58
- let err;
59
-
60
- beforeEach(function() {
61
- request = {};
62
- err = {};
63
- });
64
-
65
- it('logs at error level when 500 status code', function() {
66
- errorHandler.handler(err, request, reply);
67
- expect(reply.log.error).calledWith({ req: request, res: reply, err });
68
- });
69
-
70
- it('logs at info level when <500 status code', function() {
71
- reply.statusCode = 404;
72
- errorHandler.handler(err, request, reply);
73
- expect(reply.log.info).calledWith({ res: reply, err });
74
- });
75
- });
76
-
77
- describe('handler()', function() {
78
- let request;
79
- let err;
80
- let store;
81
- let userHandler;
82
-
83
- beforeEach(function() {
84
- request = {};
85
- err = securityException.create();
86
- store = {
87
- protect: {
88
- block: sinon.stub(),
89
- findings: {
90
- securityException: ['block', 'cmd-injection']
91
- }
92
- }
93
- };
94
- userHandler = sinon.stub();
95
- });
96
-
97
- it('calls default handler when the source context is not available', function() {
98
- errorHandler.handler(err, request, reply);
99
- expect(core.logger.info).calledWith('source context not found; unable to handle response');
100
- });
101
-
102
- it('when set, calls user handler when the source context is not available', function() {
103
- server.setErrorHandler(userHandler);
104
- errorHandler.handler(err, request, reply);
105
- expect(core.logger.info).calledWith('source context not found; unable to handle response');
106
- expect(userHandler).calledWith(err, request, reply);
107
- });
108
-
109
- it('calls source context\'s .block() with mode and id of rule which raised error', function() {
110
- const {
111
- protect: {
112
- block,
113
- findings: { securityException }
114
- }
115
- } = store;
116
- core.scopes.sources.run(store, () => {
117
- errorHandler.handler(err, request, reply);
118
- expect(core.logger.info).not.called;
119
- expect(block).calledWith(...securityException);
120
- });
121
- });
122
-
123
- it('calls default error handler when error is not security exception', function() {
124
- err = {};
125
- core.scopes.sources.run(store, () => {
126
- errorHandler.handler(err, request, reply);
127
- expect(core.logger.info).not.called;
128
- expect(errorHandler.defaultErrorHandler).calledWith(err, request, reply);
129
- });
130
- });
131
-
132
- it('when set, calls user handler when error is ont seecurity exception', function() {
133
- err = {};
134
- server.setErrorHandler(userHandler);
135
- core.scopes.sources.run(store, () => {
136
- errorHandler.handler(err, request, reply);
137
- expect(core.logger.info).not.called;
138
- expect(userHandler).calledWith(err, request, reply);
139
- });
140
- });
141
- });
142
- });
@@ -1,11 +0,0 @@
1
- import chai from 'chai';
2
- const { expect } = chai;
3
-
4
- describe('protect esm-loader', function() {
5
- it('exports the ESM hooks', async function() {
6
- const hooks = await import('./esm-loader.mjs');
7
- expect(hooks).itself.to.respondTo('getSource');
8
- expect(hooks).itself.to.respondTo('transformSource');
9
- expect(hooks).itself.to.respondTo('load');
10
- });
11
- });