Skip to content

Commit 9268b35

Browse files
feat(NODE-5233)!: prevent session from one client from being used on another (#3790)
Co-authored-by: Bailey Pearson <[email protected]>
1 parent c08060d commit 9268b35

File tree

5 files changed

+95
-3
lines changed

5 files changed

+95
-3
lines changed

Diff for: src/mongo_client.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,14 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
597597
return client.connect();
598598
}
599599

600-
/** Starts a new session on the server */
600+
/**
601+
* Creates a new ClientSession. When using the returned session in an operation
602+
* a corresponding ServerSession will be created.
603+
*
604+
* @remarks
605+
* A ClientSession instance may only be passed to operations being performed on the same
606+
* MongoClient it was started from.
607+
*/
601608
startSession(options?: ClientSessionOptions): ClientSession {
602609
const session = new ClientSession(
603610
this,

Diff for: src/operations/execute_operation.ts

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
MongoError,
88
MongoErrorLabel,
99
MongoExpiredSessionError,
10+
MongoInvalidArgumentError,
1011
MongoNetworkError,
1112
MongoNotConnectedError,
1213
MongoRuntimeError,
@@ -118,6 +119,8 @@ async function executeOperationAsync<
118119
throw new MongoExpiredSessionError('Use of expired sessions is not permitted');
119120
} else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
120121
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
122+
} else if (session.client !== client) {
123+
throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient');
121124
}
122125

123126
const readPreference = operation.readPreference ?? ReadPreference.primary;

Diff for: test/integration/sessions/sessions.prose.test.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,64 @@ import {
66
type Collection,
77
type CommandStartedEvent,
88
MongoClient,
9-
MongoDriverError
9+
MongoDriverError,
10+
MongoInvalidArgumentError
1011
} from '../../mongodb';
1112
import { sleep } from '../../tools/utils';
1213

1314
describe('Sessions Prose Tests', () => {
15+
describe('5. Session argument is for the right client', () => {
16+
let client1: MongoClient;
17+
let client2: MongoClient;
18+
beforeEach(async function () {
19+
client1 = this.configuration.newClient();
20+
client2 = this.configuration.newClient();
21+
});
22+
23+
afterEach(async function () {
24+
await client1?.close();
25+
await client2?.close();
26+
});
27+
28+
/**
29+
* Steps:
30+
* - Create client1 and client2
31+
* - Get database from client1
32+
* - Get collection from database
33+
* - Start session from client2
34+
* - Call collection.insertOne(session,...)
35+
* - Assert that an error was reported because session was not started from client1
36+
*
37+
* This validation lives in our executeOperation layer so it applies universally.
38+
* A find and an insert provide enough coverage, we determined we do not need to enumerate every possible operation.
39+
*/
40+
context(
41+
'when session is started from a different client than operation is being run on',
42+
() => {
43+
it('insertOne operation throws a MongoInvalidArgumentError', async () => {
44+
const db = client1.db();
45+
const collection = db.collection('test');
46+
const session = client2.startSession();
47+
const error = await collection.insertOne({}, { session }).catch(error => error);
48+
expect(error).to.be.instanceOf(MongoInvalidArgumentError);
49+
expect(error).to.match(/ClientSession must be from the same MongoClient/i);
50+
});
51+
52+
it('find operation throws a MongoInvalidArgumentError', async () => {
53+
const db = client1.db();
54+
const collection = db.collection('test');
55+
const session = client2.startSession();
56+
const error = await collection
57+
.find({}, { session })
58+
.toArray()
59+
.catch(error => error);
60+
expect(error).to.be.instanceOf(MongoInvalidArgumentError);
61+
expect(error).to.match(/ClientSession must be from the same MongoClient/i);
62+
});
63+
}
64+
);
65+
});
66+
1467
describe('14. Implicit sessions only allocate their server session after a successful connection checkout', () => {
1568
let client: MongoClient;
1669
let testCollection: Collection<{ _id: number; a?: number }>;

Diff for: test/integration/sessions/sessions.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from 'chai';
2+
import { MongoClient as LegacyMongoClient } from 'mongodb-legacy';
23

34
import type { CommandStartedEvent, CommandSucceededEvent, MongoClient } from '../../mongodb';
45
import { LEGACY_HELLO_COMMAND, MongoServerError } from '../../mongodb';
@@ -422,4 +423,31 @@ describe('Sessions Spec', function () {
422423
expect(new Set(events.map(ev => ev.command.lsid.id.toString('hex'))).size).to.equal(2);
423424
});
424425
});
426+
427+
context('when using a LegacyMongoClient', () => {
428+
let legacyClient;
429+
beforeEach(async function () {
430+
const options = this.configuration.serverApi
431+
? { serverApi: this.configuration.serverApi }
432+
: {};
433+
legacyClient = new LegacyMongoClient(this.configuration.url(), options);
434+
});
435+
436+
afterEach(async function () {
437+
await legacyClient?.close();
438+
});
439+
440+
it('insertOne accepts session started by legacy client', async () => {
441+
const db = legacyClient.db();
442+
const collection = db.collection('test');
443+
const session = legacyClient.startSession();
444+
const error = await collection.insertOne({}, { session }).catch(error => error);
445+
expect(error).to.not.be.instanceOf(Error);
446+
});
447+
448+
it('session returned by legacy startSession has reference to legacyClient', async () => {
449+
const session = legacyClient.startSession();
450+
expect(session).to.have.property('client', legacyClient);
451+
});
452+
});
425453
});

Diff for: test/tools/runner/config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type AuthMechanism,
88
HostAddress,
99
MongoClient,
10+
type ServerApi,
1011
TopologyType,
1112
type WriteConcernSettings
1213
} from '../../mongodb';
@@ -71,7 +72,7 @@ export class TestConfiguration {
7172
auth?: { username: string; password: string; authSource?: string };
7273
proxyURIParams?: ProxyParams;
7374
};
74-
serverApi: string;
75+
serverApi: ServerApi;
7576

7677
constructor(uri: string, context: Record<string, any>) {
7778
const url = new ConnectionString(uri);

0 commit comments

Comments
 (0)