@contrast/protect 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +9 -0
- package/lib/cli-rewriter.js +20 -0
- package/lib/error-handlers/constants.js +5 -0
- package/lib/error-handlers/index.js +13 -0
- package/lib/error-handlers/install/fastify3.js +88 -0
- package/lib/error-handlers/install/fastify3.test.js +142 -0
- package/lib/esm-loader.mjs +2 -0
- package/lib/esm-loader.test.mjs +11 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +89 -0
- package/lib/index.test.js +32 -0
- package/lib/input-analysis/handlers.js +462 -0
- package/lib/input-analysis/handlers.test.js +898 -0
- package/lib/input-analysis/index.js +16 -0
- package/lib/input-analysis/index.test.js +28 -0
- package/lib/input-analysis/install/fastify3.js +79 -0
- package/lib/input-analysis/install/fastify3.test.js +71 -0
- package/lib/input-analysis/install/http.js +185 -0
- package/lib/input-analysis/install/http.test.js +315 -0
- package/lib/input-tracing/constants.js +5 -0
- package/lib/input-tracing/handlers/index.js +117 -0
- package/lib/input-tracing/handlers/index.test.js +395 -0
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
- package/lib/input-tracing/index.js +32 -0
- package/lib/input-tracing/install/README.md +1 -0
- package/lib/input-tracing/install/child-process.js +45 -0
- package/lib/input-tracing/install/child-process.test.js +112 -0
- package/lib/input-tracing/install/fs.js +107 -0
- package/lib/input-tracing/install/fs.test.js +118 -0
- package/lib/input-tracing/install/mysql.js +57 -0
- package/lib/input-tracing/install/mysql.test.js +108 -0
- package/lib/input-tracing/install/postgres.js +61 -0
- package/lib/input-tracing/install/postgres.test.js +125 -0
- package/lib/input-tracing/install/sequelize.js +51 -0
- package/lib/input-tracing/install/sequelize.test.js +79 -0
- package/lib/input-tracing/install/sqlite3.js +45 -0
- package/lib/input-tracing/install/sqlite3.test.js +88 -0
- package/lib/make-response-blocker.js +35 -0
- package/lib/make-response-blocker.test.js +88 -0
- package/lib/make-source-context.js +130 -0
- package/lib/make-source-context.test.js +298 -0
- package/lib/security-exception.js +12 -0
- package/lib/throw-security-exception.js +30 -0
- package/lib/throw-security-exception.test.js +50 -0
- package/lib/utils.js +88 -0
- package/lib/utils.test.js +40 -0
- package/package.json +32 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function(core) {
|
|
4
|
+
const inputAnalysis = core.protect.inputAnalysis = {};
|
|
5
|
+
|
|
6
|
+
require('./handlers')(core);
|
|
7
|
+
require('./install/http')(core);
|
|
8
|
+
require('./install/fastify3')(core);
|
|
9
|
+
|
|
10
|
+
inputAnalysis.install = function() {
|
|
11
|
+
inputAnalysis.httpInstrumentation.install();
|
|
12
|
+
inputAnalysis.fastifyInstrumentation.install();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return inputAnalysis;
|
|
16
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const proxyquire = require('proxyquire');
|
|
6
|
+
|
|
7
|
+
describe('protect input-analysis', function () {
|
|
8
|
+
let modulesMock, exportsStub, coreMock;
|
|
9
|
+
beforeEach(function () {
|
|
10
|
+
exportsStub = sinon.stub();
|
|
11
|
+
modulesMock = () => exportsStub();
|
|
12
|
+
coreMock = { protect: {} };
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('calls the required hooks', function () {
|
|
16
|
+
const inputAnalysis = proxyquire('.', {
|
|
17
|
+
'./handlers': modulesMock,
|
|
18
|
+
'./install/http': modulesMock,
|
|
19
|
+
'./install/fastify3': modulesMock
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(exportsStub).to.not.have.been.called;
|
|
23
|
+
expect(coreMock.protect).to.not.haveOwnProperty('inputAnalysis');
|
|
24
|
+
inputAnalysis(coreMock);
|
|
25
|
+
expect(coreMock.protect).to.haveOwnProperty('inputAnalysis');
|
|
26
|
+
expect(exportsStub).to.have.been.callCount(3);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
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 fastify module instrumentation
|
|
19
|
+
*/
|
|
20
|
+
function install() {
|
|
21
|
+
depHooks.resolve({ name: 'fastify', version: '>=3.0.0' }, (fastify) => patchFastify(fastify));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The patch function for the depHooks callback
|
|
26
|
+
* @param {Object} fastify the fastify object returned from requiring the module
|
|
27
|
+
* @returns a patched fastify object
|
|
28
|
+
*/
|
|
29
|
+
function patchFastify(fastify) {
|
|
30
|
+
return patcher.patch(fastify, {
|
|
31
|
+
name: 'fastify.build',
|
|
32
|
+
patchType: 'framework-patch',
|
|
33
|
+
post({ result: server }) {
|
|
34
|
+
server.addHook('preValidation', preValidationHook);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fastify lifecycle hook as defined in official docs.
|
|
41
|
+
* @external https://www.fastify.io/docs/latest/Reference/Hooks/#prevalidation
|
|
42
|
+
* @param {Fastify.Request} request incoming request
|
|
43
|
+
* @param {Fastify.Reply} reply unbuilt outgoing response
|
|
44
|
+
* @param {Function} done callback to signal the hook is finished.
|
|
45
|
+
*/
|
|
46
|
+
function preValidationHook(request, reply, done) {
|
|
47
|
+
const sourceContext = sources.getStore()?.protect;
|
|
48
|
+
|
|
49
|
+
if (!sourceContext) {
|
|
50
|
+
logger.debug('source context not available in fastify prevalidation hook');
|
|
51
|
+
} else {
|
|
52
|
+
if (request.params) {
|
|
53
|
+
sourceContext.parsedParams = request.params;
|
|
54
|
+
inputAnalysis.handleUrlParams(sourceContext, request.params);
|
|
55
|
+
}
|
|
56
|
+
if (request.cookies) {
|
|
57
|
+
sourceContext.parsedCookies = request.cookies;
|
|
58
|
+
inputAnalysis.handleCookies(sourceContext, request.cookies);
|
|
59
|
+
}
|
|
60
|
+
if (request.body) {
|
|
61
|
+
sourceContext.parsedBody = request.body;
|
|
62
|
+
inputAnalysis.handleParsedBody(sourceContext, request.body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (request.query) {
|
|
66
|
+
sourceContext.parsedQuery = request.query;
|
|
67
|
+
inputAnalysis.handleQueryParams(sourceContext, request.query);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
done();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fastifyInstrumentation = inputAnalysis.fastifyInstrumentation = {
|
|
74
|
+
preValidationHook,
|
|
75
|
+
install,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return fastifyInstrumentation;
|
|
79
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const mocks = require('../../../../test/mocks');
|
|
6
|
+
|
|
7
|
+
describe('protect input-analysis fastify intrumentation', function () {
|
|
8
|
+
let core;
|
|
9
|
+
let inputAnalysis;
|
|
10
|
+
let fastify;
|
|
11
|
+
let serverMock;
|
|
12
|
+
let reqMock;
|
|
13
|
+
let resMock;
|
|
14
|
+
let doneMock;
|
|
15
|
+
|
|
16
|
+
beforeEach(function () {
|
|
17
|
+
core = mocks.core();
|
|
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
|
+
reqMock = {
|
|
25
|
+
cookies: { foo: 'bar' },
|
|
26
|
+
params: { foo: 'bar' },
|
|
27
|
+
body: { foo: 'bar' },
|
|
28
|
+
};
|
|
29
|
+
resMock = {};
|
|
30
|
+
doneMock = sinon.stub();
|
|
31
|
+
serverMock = {
|
|
32
|
+
addHook: sinon.stub().yields(reqMock, resMock, doneMock),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const fastifyOrig = () => serverMock;
|
|
36
|
+
core.depHooks.resolve.callsFake(function(desc, cb) {
|
|
37
|
+
fastify = cb(fastifyOrig);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const fastifyInstr = require('./fastify3')(core);
|
|
41
|
+
inputAnalysis = core.protect.inputAnalysis;
|
|
42
|
+
fastifyInstr.install();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('preValidationHook', function () {
|
|
46
|
+
it('hook is added and calls appropriate analysis handlers', function () {
|
|
47
|
+
core.scopes.sources.run({ protect: {} }, () => {
|
|
48
|
+
fastify();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(serverMock.addHook).to.have.been.called;
|
|
52
|
+
expect(doneMock).to.have.been.called;
|
|
53
|
+
expect(core.logger.debug).not.to.have.been.called;
|
|
54
|
+
|
|
55
|
+
expect(inputAnalysis.handleUrlParams).to.have.been.called;
|
|
56
|
+
expect(inputAnalysis.handleCookies).to.have.been.called;
|
|
57
|
+
expect(inputAnalysis.handleParsedBody).to.have.been.called;
|
|
58
|
+
});
|
|
59
|
+
it('input analysis does not occur if there is no protect source context', function () {
|
|
60
|
+
fastify();
|
|
61
|
+
expect(doneMock).to.have.been.called;
|
|
62
|
+
expect(core.logger.debug).to.have.been.calledWith(
|
|
63
|
+
'source context not available in fastify prevalidation hook'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(inputAnalysis.handleUrlParams).to.not.have.been.called;
|
|
67
|
+
expect(inputAnalysis.handleCookies).to.not.have.been.called;
|
|
68
|
+
expect(inputAnalysis.handleParsedBody).to.not.have.been.called;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Event } = require('@contrast/common');
|
|
4
|
+
|
|
5
|
+
// Instruments http `Server` and `IncomingMessage` instances to support input
|
|
6
|
+
// analysis in framework-agnostic manner.
|
|
7
|
+
|
|
8
|
+
module.exports = function(core) {
|
|
9
|
+
const { protect: { inputAnalysis } } = core;
|
|
10
|
+
inputAnalysis.httpInstrumentation = new HttpInstrumentation(core);
|
|
11
|
+
return inputAnalysis.httpInstrumentation;
|
|
12
|
+
};
|
|
13
|
+
class HttpInstrumentation {
|
|
14
|
+
constructor(core) {
|
|
15
|
+
const { logger } = core;
|
|
16
|
+
this.messages = core.messages;
|
|
17
|
+
this.scope = core.scopes.sources;
|
|
18
|
+
this.config = core.config;
|
|
19
|
+
this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
|
|
20
|
+
this.depHooks = core.depHooks;
|
|
21
|
+
this.messages = core.messages;
|
|
22
|
+
this.protect = core.protect;
|
|
23
|
+
this.makeSourceContext = this.protect.makeSourceContext;
|
|
24
|
+
this.maxBodySize = 16 * 1024 * 1024;
|
|
25
|
+
this.installed = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* After checking whether the sensor is enabled, will set up `require` hooks
|
|
30
|
+
* for instrumenting both `http` and `https` modules when they load.
|
|
31
|
+
*/
|
|
32
|
+
install() {
|
|
33
|
+
if (this.installed) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.installed = true;
|
|
38
|
+
this.hookHttp();
|
|
39
|
+
this.hookHttps();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
uninstall() {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sets hooks to instrument `http.Server.prototype`.
|
|
46
|
+
*/
|
|
47
|
+
hookHttp() {
|
|
48
|
+
this.logger.debug('hooking library: http');
|
|
49
|
+
this.depHooks.resolve({ name: 'http' }, this.hookServer.bind(this));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets hooks to instrument `https.Server.prototype`.
|
|
54
|
+
*/
|
|
55
|
+
hookHttps() {
|
|
56
|
+
this.logger.debug('hooking library: https');
|
|
57
|
+
this.depHooks.resolve({ name: 'https' }, this.hookServer.bind(this));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Instruments the `Server` prototype from `http(s)`. This patches `emit` and
|
|
62
|
+
* invokes the protect service to do analysis when appropriate.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} xport The http(s) module export
|
|
65
|
+
*/
|
|
66
|
+
hookServer(xport) {
|
|
67
|
+
const self = this;
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
Server: {
|
|
71
|
+
prototype: { emit }
|
|
72
|
+
}
|
|
73
|
+
} = xport;
|
|
74
|
+
|
|
75
|
+
xport.Server.prototype.emit = function(...args) {
|
|
76
|
+
const [type] = args;
|
|
77
|
+
|
|
78
|
+
if (type !== 'request') {
|
|
79
|
+
return emit.call(this, ...args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const context = { instance: this, method: emit, args };
|
|
83
|
+
self.initiateRequestHandling(context);
|
|
84
|
+
|
|
85
|
+
return !!this._events[type];
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates the sourceContext for the request and invokes the handler for
|
|
91
|
+
* inputs that are present in the 'incomingMessage' object at the time of
|
|
92
|
+
* the 'connect' event.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} context Function invocation context
|
|
95
|
+
*/
|
|
96
|
+
initiateRequestHandling(fnContext) {
|
|
97
|
+
const {
|
|
98
|
+
instance,
|
|
99
|
+
method,
|
|
100
|
+
args,
|
|
101
|
+
args: [, req, res]
|
|
102
|
+
} = fnContext;
|
|
103
|
+
|
|
104
|
+
// URL exclusions should be applied here. there is no point in doing any additional
|
|
105
|
+
// work if the url is excluded for a particular rule, i.e., that rule should be removed
|
|
106
|
+
// from the list of rules for this request. and if all rules are excluded for this url
|
|
107
|
+
// then none of the following needs to be done.
|
|
108
|
+
if (this.protect.rules.agentLibRulesMask === 0) {
|
|
109
|
+
this.logger.debug('no agent-lib rules are enabled, not checking request');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let store;
|
|
114
|
+
let block;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const { messages, protect: { inputAnalysis } } = this; // the functions that do input analysis
|
|
118
|
+
|
|
119
|
+
// this must be invoked by the patching code using scope.sources.run({}, ...)
|
|
120
|
+
// so that an async context is present.
|
|
121
|
+
store = this.scope.getStore();
|
|
122
|
+
// nothing can be done if async context is not available.
|
|
123
|
+
if (!store) {
|
|
124
|
+
this.logger.debug('cannot acquire store for initiateRequestHandling()');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
store.protect = this.makeSourceContext(req, res);
|
|
129
|
+
const { reqData } = store.protect;
|
|
130
|
+
|
|
131
|
+
res.on('finish', () => messages.emit(Event.PROTECT, store));
|
|
132
|
+
|
|
133
|
+
// don't put inputs in the store; they are a param to each handler. findings
|
|
134
|
+
// associated with inputs do go into the store. why not put the inputs
|
|
135
|
+
// into the store? after all, the inputs come from the store. mostly because
|
|
136
|
+
// they can really add up to a lot of data that isn't going to be used.
|
|
137
|
+
//
|
|
138
|
+
// how to replace result in resultsList, e.g., queries find something
|
|
139
|
+
// but then framework emits parsed queries? does this only matter for
|
|
140
|
+
// no-sql? index-lookup or hash?
|
|
141
|
+
//
|
|
142
|
+
// create inputs for this handler. we defer cookies until the framework
|
|
143
|
+
// parses them because there is no way to be certain of their formatting
|
|
144
|
+
// and encoding.
|
|
145
|
+
//
|
|
146
|
+
// the primary reason for this is to avoid passing the incomingMessage,
|
|
147
|
+
// req, to all the handlers allowing direct access to it and tightly
|
|
148
|
+
// coupling all handlers to an extensive collection of data.
|
|
149
|
+
const connectInputs = {
|
|
150
|
+
headers: HttpInstrumentation.removeCookies(reqData.headers),
|
|
151
|
+
uriPath: reqData.uriPath,
|
|
152
|
+
// TODO AGENT-203 - need to handle method-tampering rule.
|
|
153
|
+
method: reqData.method,
|
|
154
|
+
};
|
|
155
|
+
// only add queries if it's known that 'qs' or equivalent won't be used.
|
|
156
|
+
/* c8 ignore next 3 */
|
|
157
|
+
if (reqData.standardUrlParsing) {
|
|
158
|
+
connectInputs.queries = reqData.queries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
block = inputAnalysis.handleConnect(store.protect, connectInputs);
|
|
162
|
+
|
|
163
|
+
} catch (err) {
|
|
164
|
+
this.logger.error({ err }, 'Error during input analysis');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!block) {
|
|
168
|
+
setImmediate(() => method.call(instance, ...args));
|
|
169
|
+
} else {
|
|
170
|
+
store.protect.block(...block);
|
|
171
|
+
this.logger.debug({ block }, 'request blocked by not emitting request event');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static removeCookies(headers) {
|
|
176
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
177
|
+
if (headers[i] === 'cookies') {
|
|
178
|
+
headers = headers.slice();
|
|
179
|
+
headers.splice(i, 2);
|
|
180
|
+
return headers;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return headers;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
const { expect } = require('chai');
|
|
6
|
+
const sinon = require('sinon');
|
|
7
|
+
|
|
8
|
+
const aLib = require('@contrast/agent-lib');
|
|
9
|
+
const Protect = require('../../..');
|
|
10
|
+
const mocks = require('../../../../test/mocks');
|
|
11
|
+
|
|
12
|
+
describe('protect input-analysis/http', function() {
|
|
13
|
+
|
|
14
|
+
describe('initialization', function() {
|
|
15
|
+
let core;
|
|
16
|
+
let httpInstr;
|
|
17
|
+
|
|
18
|
+
beforeEach(function() {
|
|
19
|
+
core = mocks.core();
|
|
20
|
+
core.depHooks = mocks.depHooks();
|
|
21
|
+
core.config = mocks.config();
|
|
22
|
+
core.logger = mocks.logger();
|
|
23
|
+
core.scopes = mocks.scopes();
|
|
24
|
+
core.patcher = mocks.patcher();
|
|
25
|
+
core.protect = Protect(core);
|
|
26
|
+
httpInstr = require('./http')(core);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not be initialized after being required', function() {
|
|
30
|
+
expect(httpInstr.install).a('function');
|
|
31
|
+
expect(core.depHooks.install).not.called;
|
|
32
|
+
expect(core.depHooks.resolve).not.called;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should be initialized after being installed', function() {
|
|
36
|
+
httpInstr.install();
|
|
37
|
+
// once for http, once for https
|
|
38
|
+
expect(core.depHooks.resolve).callCount(2);
|
|
39
|
+
// no way to check the sensor state. probably should attach it to ?
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('initiateRequestHandling()', function() {
|
|
44
|
+
let core;
|
|
45
|
+
let expectedRules;
|
|
46
|
+
let httpInstr;
|
|
47
|
+
let server;
|
|
48
|
+
let serverEmit;
|
|
49
|
+
let req, res;
|
|
50
|
+
let reqEmitter;
|
|
51
|
+
let context;
|
|
52
|
+
let expectedReqData;
|
|
53
|
+
let connectInputs;
|
|
54
|
+
|
|
55
|
+
beforeEach(function() {
|
|
56
|
+
//console.log(this.currentTest.title);
|
|
57
|
+
core = mocks.core();
|
|
58
|
+
core.depHooks = mocks.depHooks();
|
|
59
|
+
core.config = mocks.config();
|
|
60
|
+
Object.assign(core.config.protect.rules, {
|
|
61
|
+
'cmd-injection': { mode: 'monitor' }
|
|
62
|
+
});
|
|
63
|
+
core.logger = mocks.logger();
|
|
64
|
+
core.scopes = mocks.scopes();
|
|
65
|
+
core.patcher = mocks.patcher();
|
|
66
|
+
core.protect = Protect(core);
|
|
67
|
+
|
|
68
|
+
expectedRules = makeExpectedRules(core.config.protect.rules);
|
|
69
|
+
|
|
70
|
+
sinon.spy(core.protect.inputAnalysis, 'handleConnect');
|
|
71
|
+
|
|
72
|
+
httpInstr = require('./http')(core);
|
|
73
|
+
sinon.spy(httpInstr, 'makeSourceContext');
|
|
74
|
+
sinon.spy(httpInstr.scope, 'getStore');
|
|
75
|
+
|
|
76
|
+
// mock Server thing...
|
|
77
|
+
server = {};
|
|
78
|
+
serverEmit = sinon.stub();
|
|
79
|
+
server.prototype = { emit: serverEmit };
|
|
80
|
+
|
|
81
|
+
// mock req, res
|
|
82
|
+
reqEmitter = new EventEmitter();
|
|
83
|
+
req = {
|
|
84
|
+
url: '/',
|
|
85
|
+
method: 'GET',
|
|
86
|
+
rawHeaders: ['host', 'vogon.com', 'content-type', 'application/json'],
|
|
87
|
+
// this needs to look sort of like a real incoming message
|
|
88
|
+
emit: (...args) => reqEmitter.emit(...args),
|
|
89
|
+
_events: {},
|
|
90
|
+
socket: { _readableState: { autoDestroy: false } },
|
|
91
|
+
resume: sinon.stub(),
|
|
92
|
+
};
|
|
93
|
+
res = {
|
|
94
|
+
end: sinon.stub(),
|
|
95
|
+
writeHead: sinon.stub(),
|
|
96
|
+
headersSent: false,
|
|
97
|
+
on: sinon.stub(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// setup the context required
|
|
101
|
+
context = {
|
|
102
|
+
instance: server,
|
|
103
|
+
method: serverEmit,
|
|
104
|
+
args: ['request', req, res]
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// setup a few expected results. these can be modified as needed by
|
|
108
|
+
// different tests.
|
|
109
|
+
expectedReqData = {
|
|
110
|
+
method: req.method,
|
|
111
|
+
headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
|
|
112
|
+
uriPath: '/',
|
|
113
|
+
queries: '',
|
|
114
|
+
contentType: '',
|
|
115
|
+
standardUrlParsing: false,
|
|
116
|
+
};
|
|
117
|
+
connectInputs = {
|
|
118
|
+
method: expectedReqData.method,
|
|
119
|
+
headers: expectedReqData.headers,
|
|
120
|
+
uriPath: expectedReqData.uriPath,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
//
|
|
125
|
+
// make sure basic things are as expected
|
|
126
|
+
//
|
|
127
|
+
function doBasicChecks(expectedReqData) {
|
|
128
|
+
// scope.run() call invokes scope.getStore(), so it will be called once to execute
|
|
129
|
+
// the test and once more unless no rules were in effect.
|
|
130
|
+
if (core.protect.rules.agentLibRulesMask !== 0) {
|
|
131
|
+
expect(httpInstr.scope.getStore).callCount(2);
|
|
132
|
+
expect(httpInstr.makeSourceContext).calledOnceWith(req, res);
|
|
133
|
+
} else {
|
|
134
|
+
// source context wasn't available, so
|
|
135
|
+
expect(httpInstr.scope.getStore).callCount(1);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sourceContext = httpInstr.makeSourceContext.returnValues[0];
|
|
140
|
+
const {
|
|
141
|
+
block, exclusions, findings, reqData, rules, virtualPatches
|
|
142
|
+
} = sourceContext;
|
|
143
|
+
|
|
144
|
+
/* eslint-disable newline-per-chained-call */
|
|
145
|
+
expect(block).a('function');
|
|
146
|
+
expect(exclusions).an('array').eql([]);
|
|
147
|
+
expect(findings).an('object').eql({
|
|
148
|
+
trackRequest: false, securityException: undefined, bodyType: undefined, resultsMap: {}
|
|
149
|
+
});
|
|
150
|
+
expect(reqData.method).equal(expectedReqData.method);
|
|
151
|
+
expect(reqData.uriPath).equal(expectedReqData.uriPath);
|
|
152
|
+
expect(reqData.headers).eql(expectedReqData.headers);
|
|
153
|
+
expect(rules).an('object').eql(expectedRules);
|
|
154
|
+
expect(virtualPatches).an('array').eql([]);
|
|
155
|
+
|
|
156
|
+
return sourceContext;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// i don't see how to unit test this; xport.Server.prototype.emit()
|
|
160
|
+
// i.e., the real patching function. i think verifying that function
|
|
161
|
+
// will require an integration test of some sort.
|
|
162
|
+
// it('should handle requests on http, https, and http2', ...)
|
|
163
|
+
|
|
164
|
+
// initiateRequestHandling() sets up everything but the inputs for all analysis
|
|
165
|
+
// of this request.
|
|
166
|
+
it('works as expected', function(done) {
|
|
167
|
+
// this is called here because other tests will modify the default data setup in
|
|
168
|
+
// beforeEach().
|
|
169
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
170
|
+
|
|
171
|
+
const sourceContext = doBasicChecks(expectedReqData);
|
|
172
|
+
|
|
173
|
+
// and finally, handleConnect() should have been called with context
|
|
174
|
+
// and input.
|
|
175
|
+
expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
|
|
176
|
+
|
|
177
|
+
// the real emitter is called via setImmediate() so give it a chance to run.
|
|
178
|
+
expect(serverEmit).callCount(0);
|
|
179
|
+
setImmediate(function() {
|
|
180
|
+
expect(serverEmit).callCount(1);
|
|
181
|
+
done();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('debug logs if no async context', function() {
|
|
186
|
+
// this is called here because other tests will modify the default data setup in
|
|
187
|
+
// beforeEach().
|
|
188
|
+
httpInstr.scope.getStore.restore();
|
|
189
|
+
sinon.stub(httpInstr.scope, 'getStore').returns(undefined);
|
|
190
|
+
core.logger.debug = sinon.stub();
|
|
191
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
192
|
+
|
|
193
|
+
expect(core.logger.debug).calledOnceWith('cannot acquire store for initiateRequestHandling()');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('error logs on exceptions', function() {
|
|
197
|
+
const err = new Error('sometimes things do not work out');
|
|
198
|
+
const { inputAnalysis } = core.protect;
|
|
199
|
+
inputAnalysis.handleConnect.restore();
|
|
200
|
+
sinon.stub(inputAnalysis, 'handleConnect').throws(err);
|
|
201
|
+
// simulate an incoming request to the server.
|
|
202
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
203
|
+
|
|
204
|
+
expect(core.logger.error).calledOnceWith({ err }, 'Error during input analysis');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('connectInputs will contain not a cookies header', function(done) {
|
|
208
|
+
const withoutCookies = req.rawHeaders.slice();
|
|
209
|
+
req.rawHeaders.push('COOKIES', 'this;that;the other;thing');
|
|
210
|
+
expectedReqData.headers.push('cookies', 'this;that;the other;thing');
|
|
211
|
+
|
|
212
|
+
// simulate an incoming request to the server.
|
|
213
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
214
|
+
|
|
215
|
+
const sourceContext = doBasicChecks(expectedReqData);
|
|
216
|
+
|
|
217
|
+
// and finally, handleConnect() should have been called with sourceContext and connectInput.
|
|
218
|
+
// the cookies header should not be present because protect waits until the framework has
|
|
219
|
+
// decoded the cookies before scoring them.
|
|
220
|
+
connectInputs.headers = withoutCookies;
|
|
221
|
+
expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
|
|
222
|
+
|
|
223
|
+
// the real emitter is called via setImmediate() so give it a chance
|
|
224
|
+
// to run.
|
|
225
|
+
expect(serverEmit).callCount(0);
|
|
226
|
+
setImmediate(function() {
|
|
227
|
+
expect(serverEmit).callCount(1);
|
|
228
|
+
done();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('does not analyze the request when no rules are in effect', function() {
|
|
233
|
+
// kind of hacky reset on the rules
|
|
234
|
+
core.protect.rules = {
|
|
235
|
+
agentLibRulesMask: 0,
|
|
236
|
+
agentLibRules: {},
|
|
237
|
+
agentRules: {},
|
|
238
|
+
};
|
|
239
|
+
expectedRules = core.protect.rules;
|
|
240
|
+
core.logger.debug = sinon.stub();
|
|
241
|
+
|
|
242
|
+
// simulate an incoming request to the server.
|
|
243
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
244
|
+
|
|
245
|
+
doBasicChecks(expectedReqData);
|
|
246
|
+
|
|
247
|
+
expect(core.logger.debug).calledOnceWith('no agent-lib rules are enabled, not checking request');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('does not emit the original event when blocked', function(done) {
|
|
251
|
+
req.url = '/need/<script%20this&that';
|
|
252
|
+
const sourceContext = core.protect.makeSourceContext(req, res);
|
|
253
|
+
sourceContext.rules.agentLibRules['reflected-xss'] = { mode: 'block' };
|
|
254
|
+
sourceContext.rules.agentLibRulesMask |= aLib.constants.RuleType['reflected-xss'];
|
|
255
|
+
sourceContext.block = sinon.stub();
|
|
256
|
+
core.logger.debug = sinon.stub();
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
260
|
+
|
|
261
|
+
const block = ['block', 'reflected-xss'];
|
|
262
|
+
|
|
263
|
+
expect(core.protect.inputAnalysis.handleConnect).callCount(1).returned(block);
|
|
264
|
+
expect(core.logger.debug).calledWith({ block }, 'request blocked by not emitting request event');
|
|
265
|
+
setImmediate(function() {
|
|
266
|
+
expect(serverEmit).callCount(0);
|
|
267
|
+
done();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('does not handle a request body when it is not JSON', function(done) {
|
|
272
|
+
req.rawHeaders = ['Content-Type', 'multipart/form-data'];
|
|
273
|
+
expectedReqData.headers = ['content-type', 'multipart/form-data'];
|
|
274
|
+
expectedReqData.contentType = 'multipart/form-data';
|
|
275
|
+
|
|
276
|
+
// simulate an incoming request to the server.
|
|
277
|
+
httpInstr.scope.run(httpInstr.scope, () => httpInstr.initiateRequestHandling(context));
|
|
278
|
+
|
|
279
|
+
const sourceContext = doBasicChecks(expectedReqData);
|
|
280
|
+
|
|
281
|
+
// and finally, handleConnect() should have been called with sourceContext and connectInput.
|
|
282
|
+
connectInputs.headers = expectedReqData.headers;
|
|
283
|
+
expect(core.protect.inputAnalysis.handleConnect).calledOnceWith(sourceContext, connectInputs);
|
|
284
|
+
|
|
285
|
+
expect(serverEmit).callCount(0);
|
|
286
|
+
setImmediate(function() {
|
|
287
|
+
expect(serverEmit).callCount(1);
|
|
288
|
+
done();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
function makeExpectedRules(rules) {
|
|
295
|
+
const { RuleType } = aLib.constants;
|
|
296
|
+
const agentLibRules = {};
|
|
297
|
+
const agentRules = {};
|
|
298
|
+
let agentLibRulesMask = 0;
|
|
299
|
+
|
|
300
|
+
for (const rule in rules) {
|
|
301
|
+
if (!(rule in RuleType)) {
|
|
302
|
+
// it's a little random, but if the rule isn't an agent-lib rule, presume
|
|
303
|
+
// that it's an agent rule.
|
|
304
|
+
if (rule !== 'disabled_rules') {
|
|
305
|
+
agentRules[rule] = { mode: undefined };
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
agentLibRules[rule] = rules[rule];
|
|
310
|
+
agentLibRulesMask |= RuleType[rule];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { agentLibRules, agentLibRulesMask, agentRules };
|
|
314
|
+
|
|
315
|
+
}
|