Skip to content

Commit 750760c

Browse files
W-A-Jamesdariakp
andauthored
fix(NODE-3358): Command monitoring objects hold internal state references (#2858)
Co-authored-by: Daria Pardue <81593090+dariakp@users.noreply.github.com>
1 parent a917dfa commit 750760c

File tree

4 files changed

+202
-7
lines changed

4 files changed

+202
-7
lines changed

lib/command_utils.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const Msg = require('./core/connection/msg').Msg;
33
const KillCursor = require('./core/connection/commands').KillCursor;
44
const GetMore = require('./core/connection/commands').GetMore;
5+
const deepCopy = require('./utils').deepCopy;
56

67
/** Commands that we want to redact because of the sensitive nature of their contents */
78
const SENSITIVE_COMMANDS = new Set([
@@ -63,17 +64,17 @@ const extractCommand = command => {
6364
let extractedCommand;
6465
if (command instanceof GetMore) {
6566
extractedCommand = {
66-
getMore: command.cursorId,
67+
getMore: deepCopy(command.cursorId),
6768
collection: collectionName(command),
6869
batchSize: command.numberToReturn
6970
};
7071
} else if (command instanceof KillCursor) {
7172
extractedCommand = {
7273
killCursors: collectionName(command),
73-
cursors: command.cursorIds
74+
cursors: deepCopy(command.cursorIds)
7475
};
7576
} else if (command instanceof Msg) {
76-
extractedCommand = command.command;
77+
extractedCommand = deepCopy(command.command);
7778
} else if (command.query && command.query.$query) {
7879
let result;
7980
if (command.ns === 'admin.$cmd') {
@@ -84,12 +85,13 @@ const extractCommand = command => {
8485
result = { find: collectionName(command) };
8586
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
8687
if (typeof command.query[key] !== 'undefined')
87-
result[LEGACY_FIND_QUERY_MAP[key]] = command.query[key];
88+
result[LEGACY_FIND_QUERY_MAP[key]] = deepCopy(command.query[key]);
8889
});
8990
}
9091

9192
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
92-
if (typeof command[key] !== 'undefined') result[LEGACY_FIND_OPTIONS_MAP[key]] = command[key];
93+
if (typeof command[key] !== 'undefined')
94+
result[LEGACY_FIND_OPTIONS_MAP[key]] = deepCopy(command[key]);
9395
});
9496

9597
OP_QUERY_KEYS.forEach(key => {
@@ -106,7 +108,7 @@ const extractCommand = command => {
106108
extractedCommand = result;
107109
}
108110
} else {
109-
extractedCommand = command.query || command;
111+
extractedCommand = deepCopy(command.query || command);
110112
}
111113

112114
const commandName = Object.keys(extractedCommand)[0];

lib/utils.js

+78-1
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,82 @@ function emitWarningOnce(message) {
871871
}
872872
}
873873

874+
function isSuperset(set, subset) {
875+
set = Array.isArray(set) ? new Set(set) : set;
876+
subset = Array.isArray(subset) ? new Set(subset) : subset;
877+
for (const elem of subset) {
878+
if (!set.has(elem)) {
879+
return false;
880+
}
881+
}
882+
return true;
883+
}
884+
885+
function isRecord(value, requiredKeys) {
886+
const toString = Object.prototype.toString;
887+
const hasOwnProperty = Object.prototype.hasOwnProperty;
888+
const isObject = v => toString.call(v) === '[object Object]';
889+
if (!isObject(value)) {
890+
return false;
891+
}
892+
893+
const ctor = value.constructor;
894+
if (ctor && ctor.prototype) {
895+
if (!isObject(ctor.prototype)) {
896+
return false;
897+
}
898+
899+
// Check to see if some method exists from the Object exists
900+
if (!hasOwnProperty.call(ctor.prototype, 'isPrototypeOf')) {
901+
return false;
902+
}
903+
}
904+
905+
if (requiredKeys) {
906+
const keys = Object.keys(value);
907+
return isSuperset(keys, requiredKeys);
908+
}
909+
910+
return true;
911+
}
912+
913+
/**
914+
* Make a deep copy of an object
915+
*
916+
* NOTE: This is not meant to be the perfect implementation of a deep copy,
917+
* but instead something that is good enough for the purposes of
918+
* command monitoring.
919+
*/
920+
function deepCopy(value) {
921+
if (value == null) {
922+
return value;
923+
} else if (Array.isArray(value)) {
924+
return value.map(item => deepCopy(item));
925+
} else if (isRecord(value)) {
926+
const res = {};
927+
for (const key in value) {
928+
res[key] = deepCopy(value[key]);
929+
}
930+
return res;
931+
}
932+
933+
const ctor = value.constructor;
934+
if (ctor) {
935+
switch (ctor.name.toLowerCase()) {
936+
case 'date':
937+
return new ctor(Number(value));
938+
case 'map':
939+
return new Map(value);
940+
case 'set':
941+
return new Set(value);
942+
case 'buffer':
943+
return Buffer.from(value);
944+
}
945+
}
946+
947+
return value;
948+
}
949+
874950
module.exports = {
875951
filterOptions,
876952
mergeOptions,
@@ -907,5 +983,6 @@ module.exports = {
907983
hasAtomicOperators,
908984
MONGODB_WARNING_CODE,
909985
emitWarning,
910-
emitWarningOnce
986+
emitWarningOnce,
987+
deepCopy
911988
};

test/functional/apm.test.js

+49
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,55 @@ describe('APM', function() {
701701
}
702702
});
703703

704+
// NODE-3358
705+
describe('Internal state references', function() {
706+
let client;
707+
708+
beforeEach(function() {
709+
client = this.configuration.newClient(
710+
{ writeConcern: { w: 1 } },
711+
{ maxPoolSize: 1, monitorCommands: true }
712+
);
713+
});
714+
715+
afterEach(function(done) {
716+
client.close(done);
717+
});
718+
719+
it('should not allow mutation of internal state from commands returned by event monitoring', function() {
720+
const started = [];
721+
const succeeded = [];
722+
const documentToInsert = { a: { b: 1 } };
723+
client.on('commandStarted', filterForCommands('insert', started));
724+
client.on('commandSucceeded', filterForCommands('insert', succeeded));
725+
return client
726+
.connect()
727+
.then(client => {
728+
const db = client.db(this.configuration.db);
729+
return db.collection('apm_test').insertOne(documentToInsert);
730+
})
731+
.then(r => {
732+
expect(r)
733+
.to.have.property('insertedId')
734+
.that.is.an('object');
735+
expect(started).to.have.lengthOf(1);
736+
// Check if contents of returned document are equal to document inserted (by value)
737+
expect(documentToInsert).to.deep.equal(started[0].command.documents[0]);
738+
// Check if the returned document is a clone of the original. This confirms that the
739+
// reference is not the same.
740+
expect(documentToInsert !== started[0].command.documents[0]).to.equal(true);
741+
expect(documentToInsert.a !== started[0].command.documents[0].a).to.equal(true);
742+
743+
started[0].command.documents[0].a.b = 2;
744+
expect(documentToInsert.a.b).to.equal(1);
745+
746+
expect(started[0].commandName).to.equal('insert');
747+
expect(started[0].command.insert).to.equal('apm_test');
748+
expect(succeeded).to.have.lengthOf(1);
749+
});
750+
});
751+
});
752+
704753
it('should correctly receive the APM events for deleteOne', {
705754
metadata: { requires: { topology: ['single', 'replicaset'] } },
706755

test/unit/cmap/apm_events.test.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
const Msg = require('../../../lib/core/connection/msg').Msg;
4+
const GetMore = require('../../../lib/core/connection/commands').GetMore;
5+
const Query = require('../../../lib/core/connection/commands').Query;
6+
const KillCursor = require('../../../lib/core/connection/commands').KillCursor;
7+
const CommandStartedEvent = require('../../../lib/core/connection/apm').CommandStartedEvent;
8+
const expect = require('chai').expect;
9+
const Long = require('bson').Long;
10+
11+
describe('Command Monitoring Events - unit/cmap', function() {
12+
const commands = [
13+
new Query({}, 'admin.$cmd', { a: { b: 10 }, $query: { b: 10 } }, {}),
14+
new Query({}, 'hello', { a: { b: 10 }, $query: { b: 10 } }, {}),
15+
new Msg({}, 'admin.$cmd', { b: { c: 20 } }, {}),
16+
new Msg({}, 'hello', { b: { c: 20 } }, {}),
17+
new GetMore({}, 'admin.$cmd', Long.fromNumber(10)),
18+
new GetMore({}, 'hello', Long.fromNumber(10)),
19+
new KillCursor({}, 'admin.$cmd', [Long.fromNumber(100), Long.fromNumber(200)]),
20+
new KillCursor({}, 'hello', [Long.fromNumber(100), Long.fromNumber(200)]),
21+
{ ns: 'admin.$cmd', query: { $query: { a: 16 } } },
22+
{ ns: 'hello there', f1: { h: { a: 52, b: { c: 10, d: [1, 2, 3, 5] } } } }
23+
];
24+
25+
for (const command of commands) {
26+
it(`CommandStartedEvent should make a deep copy of object of type: ${command.constructor.name}`, () => {
27+
const ev = new CommandStartedEvent({ id: 'someId', address: 'someHost' }, command);
28+
if (command instanceof Query) {
29+
if (command.ns === 'admin.$cmd') {
30+
expect(ev.command !== command.query.$query).to.equal(true);
31+
for (const k in command.query.$query) {
32+
expect(ev.command[k]).to.deep.equal(command.query.$query[k]);
33+
}
34+
} else {
35+
expect(ev.command.filter !== command.query.$query).to.equal(true);
36+
for (const k in command.query.$query) {
37+
expect(ev.command.filter[k]).to.deep.equal(command.query.$query[k]);
38+
}
39+
}
40+
} else if (command instanceof Msg) {
41+
expect(ev.command !== command.command).to.equal(true);
42+
expect(ev.command).to.deep.equal(command.command);
43+
} else if (command instanceof GetMore) {
44+
// NOTE: BSON Longs pass strict equality when their internal values are equal
45+
// i.e.
46+
// let l1 = Long(10);
47+
// let l2 = Long(10);
48+
// l1 === l2 // returns true
49+
// expect(ev.command.getMore !== command.cursorId).to.equal(true);
50+
expect(ev.command.getMore).to.deep.equal(command.cursorId);
51+
52+
ev.command.getMore = Long.fromNumber(50128);
53+
expect(command.cursorId).to.not.deep.equal(ev.command.getMore);
54+
} else if (command instanceof KillCursor) {
55+
expect(ev.command.cursors !== command.cursorIds).to.equal(true);
56+
expect(ev.command.cursors).to.deep.equal(command.cursorIds);
57+
} else if (typeof command === 'object') {
58+
if (command.ns === 'admin.$cmd') {
59+
expect(ev.command !== command.query.$query).to.equal(true);
60+
for (const k in command.query.$query) {
61+
expect(ev.command[k]).to.deep.equal(command.query.$query[k]);
62+
}
63+
}
64+
}
65+
});
66+
}
67+
});

0 commit comments

Comments
 (0)