@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.
- package/lib/error-handlers/index.js +2 -0
- package/lib/error-handlers/install/koa2.js +53 -0
- package/lib/input-analysis/index.js +2 -0
- package/lib/input-analysis/install/co-body.js +51 -0
- package/lib/input-analysis/install/cookie-parser.js +48 -0
- package/lib/input-analysis/install/formidable.js +53 -0
- package/lib/input-analysis/install/koa2.js +137 -0
- package/lib/input-analysis/install/multer.js +52 -0
- package/lib/input-analysis/install/qs.js +40 -0
- package/lib/input-analysis/install/universal-cookie.js +34 -0
- package/package.json +4 -3
- package/lib/error-handlers/install/fastify3.test.js +0 -142
- package/lib/esm-loader.test.mjs +0 -11
- package/lib/index.test.js +0 -32
- package/lib/input-analysis/handlers.test.js +0 -898
- package/lib/input-analysis/index.test.js +0 -28
- package/lib/input-analysis/install/fastify3.test.js +0 -71
- package/lib/input-analysis/install/http.test.js +0 -315
- package/lib/input-tracing/handlers/index.test.js +0 -395
- package/lib/input-tracing/install/child-process.test.js +0 -112
- package/lib/input-tracing/install/fs.test.js +0 -118
- package/lib/input-tracing/install/mysql.test.js +0 -108
- package/lib/input-tracing/install/postgres.test.js +0 -125
- package/lib/input-tracing/install/sequelize.test.js +0 -79
- package/lib/input-tracing/install/sqlite3.test.js +0 -88
- package/lib/make-response-blocker.test.js +0 -88
- package/lib/make-source-context.test.js +0 -298
- package/lib/throw-security-exception.test.js +0 -50
- package/lib/utils.test.js +0 -40
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,315 +0,0 @@
|
|
|
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
|
-
}
|