Skip to content

Commit 142b568

Browse files
committed
fix: instruct models not to generate passwords for createDBUser
1 parent d585baa commit 142b568

File tree

5 files changed

+64
-20
lines changed

5 files changed

+64
-20
lines changed

src/common/atlas/generatePassword.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { randomBytes } from "crypto";
2+
import { promisify } from "util";
3+
4+
const randomBytesAsync = promisify(randomBytes);
5+
6+
export async function generateSecurePassword(): Promise<string> {
7+
const buf = await randomBytesAsync(16);
8+
const pass = buf.toString("base64url");
9+
return pass;
10+
}

src/tools/atlas/create/createDBUser.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55
import { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
6+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
67

78
export class CreateDBUserTool extends AtlasToolBase {
89
protected name = "atlas-create-db-user";
@@ -11,7 +12,16 @@ export class CreateDBUserTool extends AtlasToolBase {
1112
protected argsShape = {
1213
projectId: z.string().describe("Atlas project ID"),
1314
username: z.string().describe("Username for the new user"),
14-
password: z.string().describe("Password for the new user"),
15+
// Models will generate overly simplistic passwords like SecurePassword123 or
16+
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
17+
// the model not to try and generate anything and instead leave the field unset.
18+
password: z
19+
.string()
20+
.optional()
21+
.nullable()
22+
.describe(
23+
"Password for the new user. If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
24+
),
1525
roles: z
1626
.array(
1727
z.object({
@@ -34,6 +44,11 @@ export class CreateDBUserTool extends AtlasToolBase {
3444
roles,
3545
clusters,
3646
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
47+
const shouldGeneratePassword = !password;
48+
if (shouldGeneratePassword) {
49+
password = await generateSecurePassword();
50+
}
51+
3752
const input = {
3853
groupId: projectId,
3954
awsIAMType: "NONE",
@@ -62,7 +77,12 @@ export class CreateDBUserTool extends AtlasToolBase {
6277
});
6378

6479
return {
65-
content: [{ type: "text", text: `User "${username}" created sucessfully.` }],
80+
content: [
81+
{
82+
type: "text",
83+
text: `User "${username}" created sucessfully${shouldGeneratePassword ? ` with password: \`${password}\`` : ""}.`,
84+
},
85+
],
6686
};
6787
}
6888
}

src/tools/atlas/metadata/connectCluster.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,10 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
5-
import { randomBytes } from "crypto";
6-
import { promisify } from "util";
5+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
76

87
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
98

10-
const randomBytesAsync = promisify(randomBytes);
11-
12-
async function generateSecurePassword(): Promise<string> {
13-
const buf = await randomBytesAsync(16);
14-
const pass = buf.toString("base64url");
15-
return pass;
16-
}
17-
189
export class ConnectClusterTool extends AtlasToolBase {
1910
protected name = "atlas-connect-cluster";
2011
protected description = "Connect to MongoDB Atlas cluster";

tests/integration/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
117117
content = (content as { content: unknown }).content;
118118
}
119119

120-
expect(Array.isArray(content)).toBe(true);
120+
expect(content).toBeArray();
121121

122122
const response = content as { type: string; text: string }[];
123123
for (const item of response) {

tests/integration/tools/atlas/dbUsers.test.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { Session } from "../../../../src/session.js";
33
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
4-
import { expectDefined } from "../../helpers.js";
4+
import { expectDefined, getResponseElements } from "../../helpers.js";
55

66
describeWithAtlas("db users", (integration) => {
77
const userName = "testuser-" + randomId;
@@ -34,10 +34,11 @@ describeWithAtlas("db users", (integration) => {
3434
expect(createDbUser.inputSchema.properties).toHaveProperty("roles");
3535
expect(createDbUser.inputSchema.properties).toHaveProperty("clusters");
3636
});
37-
it("should create a database user", async () => {
37+
38+
it("should create a database user with supplied password", async () => {
3839
const projectId = getProjectId();
3940

40-
const response = (await integration.mcpClient().callTool({
41+
const response = await integration.mcpClient().callTool({
4142
name: "atlas-create-db-user",
4243
arguments: {
4344
projectId,
@@ -50,10 +51,32 @@ describeWithAtlas("db users", (integration) => {
5051
},
5152
],
5253
},
53-
})) as CallToolResult;
54-
expect(response.content).toBeArray();
55-
expect(response.content).toHaveLength(1);
56-
expect(response.content[0].text).toContain("created sucessfully");
54+
});
55+
const elements = getResponseElements(response);
56+
expect(elements).toHaveLength(1);
57+
expect(elements[0].text).toContain("created sucessfully");
58+
});
59+
60+
it("should create a database user with generated password", async () => {
61+
const projectId = getProjectId();
62+
63+
const response = await integration.mcpClient().callTool({
64+
name: "atlas-create-db-user",
65+
arguments: {
66+
projectId,
67+
username: userName,
68+
roles: [
69+
{
70+
roleName: "readWrite",
71+
databaseName: "admin",
72+
},
73+
],
74+
},
75+
});
76+
const elements = getResponseElements(response);
77+
expect(elements).toHaveLength(1);
78+
expect(elements[0].text).toContain("created sucessfully");
79+
expect(elements[0].text).toContain("with password: `");
5780
});
5881
});
5982
describe("atlas-list-db-users", () => {

0 commit comments

Comments
 (0)