fix(mongodb): validate options and bound findOne queries

This commit is contained in:
Võ Văn Thành (Jay) authored and GitHub committed 2026-07-15 13:19:53 +08:00
1 parent d098e3e970
commit 55f15c8e4b
14 files changed
+934 -5

No files matched your search

+127 -3
View File
@@ -10,6 +10,13 @@ export interface MongoFindCommand {
sort?: string;
}
export interface MongoFindOneCommand {
collection: string;
filter: string;
projection?: string;
options?: string;
}
export interface MongoCountDocumentsCommand {
collection: string;
filter: string;
@@ -41,10 +48,11 @@ export interface MongoCollectionStatsCommand {
scale?: number;
}
type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection";
type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete";
export type MongoCommand =
| ({ kind: "find" } & MongoFindCommand)
| ({ kind: "findOne" } & MongoFindOneCommand)
| MongoVersionCommand
| ({ kind: "countDocuments" } & MongoCountDocumentsCommand)
| ({ kind: "aggregate" } & MongoAggregateCommand)
@@ -57,7 +65,10 @@ export type MongoCommand =
| { kind: "createIndex"; collection: string; keys: string; options?: string }
| { kind: "dropIndex"; collection: string; index: string }
| { kind: "dropIndexes"; collection: string; indexes?: string }
| { kind: "dropCollection"; collection: string };
| { kind: "dropCollection"; collection: string }
| { kind: "findOneAndUpdate"; collection: string; filter: string; update: string; options?: string }
| { kind: "findOneAndReplace"; collection: string; filter: string; replacement: string; options?: string }
| { kind: "findOneAndDelete"; collection: string; filter: string; options?: string };
export type MongoWriteCommand = Extract<MongoCommand, { kind: MongoWriteKind }>;
@@ -124,6 +135,103 @@ export function parseMongoFindCommand(input: string): MongoFindCommand | null {
};
}
export function parseMongoFindOneCommand(input: string): MongoFindOneCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseCollectionMethodTarget(source, "findOne");
if (!target) return null;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args) return null;
if (args.length > 3 && args.slice(3).some((arg) => arg.trim())) return null;
const filter = normalizeJsonArgument(args[0] || "{}");
if (!filter) return null;
let projection: string | undefined;
if (args[1]?.trim()) {
const parsedProjection = normalizeJsonArgument(args[1]);
if (!parsedProjection) return null;
projection = parsedProjection;
}
const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined;
if (args[2]?.trim() && !options) return null;
return {
collection: target.collection,
filter,
...(projection ? { projection } : {}),
...(options ? { options } : {}),
};
}
export interface MongoFindOneAndUpdateCommand {
collection: string;
filter: string;
update: string;
options?: string;
}
export interface MongoFindOneAndReplaceCommand {
collection: string;
filter: string;
replacement: string;
options?: string;
}
export interface MongoFindOneAndDeleteCommand {
collection: string;
filter: string;
options?: string;
}
export function parseMongoFindOneAndUpdateCommand(input: string): MongoFindOneAndUpdateCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseCollectionMethodTarget(source, "findOneAndUpdate");
if (!target) return null;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args || args.length < 2 || args.length > 3) return null;
const filter = normalizeJsonArgument(args[0] || "{}");
const update = normalizeJsonArgument(args[1]);
if (!filter || !update) return null;
const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined;
if (args[2]?.trim() && !options) return null;
return { collection: target.collection, filter, update, ...(options ? { options } : {}) };
}
export function parseMongoFindOneAndReplaceCommand(input: string): MongoFindOneAndReplaceCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseCollectionMethodTarget(source, "findOneAndReplace");
if (!target) return null;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args || args.length < 2 || args.length > 3) return null;
const filter = normalizeJsonArgument(args[0] || "{}");
const replacement = normalizeJsonArgument(args[1]);
if (!filter || !replacement) return null;
const options = args[2]?.trim() ? normalizeJsonArgument(args[2]) : undefined;
if (args[2]?.trim() && !options) return null;
return { collection: target.collection, filter, replacement, ...(options ? { options } : {}) };
}
export function parseMongoFindOneAndDeleteCommand(input: string): MongoFindOneAndDeleteCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseCollectionMethodTarget(source, "findOneAndDelete");
if (!target) return null;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args || args.length < 1 || args.length > 2) return null;
const filter = normalizeJsonArgument(args[0] || "{}");
if (!filter) return null;
const options = args[1]?.trim() ? normalizeJsonArgument(args[1]) : undefined;
if (args[1]?.trim() && !options) return null;
return { collection: target.collection, filter, ...(options ? { options } : {}) };
}
export function applyMongoFindSort(input: string, column: string, direction: "asc" | "desc"): string | null {
const source = input.trim().replace(/;$/, "").trim();
const parsed = parseMongoFindCommand(source);
@@ -376,6 +484,22 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null {
const find = parseMongoFindCommand(source);
return find ? { kind: "find", ...find } : null;
},
(source) => {
const findOne = parseMongoFindOneCommand(source);
return findOne ? { kind: "findOne", ...findOne } : null;
},
(source) => {
const findOneAndUpdate = parseMongoFindOneAndUpdateCommand(source);
return findOneAndUpdate ? { kind: "findOneAndUpdate", ...findOneAndUpdate } : null;
},
(source) => {
const findOneAndReplace = parseMongoFindOneAndReplaceCommand(source);
return findOneAndReplace ? { kind: "findOneAndReplace", ...findOneAndReplace } : null;
},
(source) => {
const findOneAndDelete = parseMongoFindOneAndDeleteCommand(source);
return findOneAndDelete ? { kind: "findOneAndDelete", ...findOneAndDelete } : null;
},
(source) => {
const aggregate = parseMongoAggregateCommand(source);
return aggregate ? { kind: "aggregate", ...aggregate } : null;
@@ -427,7 +551,7 @@ export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: Mo
reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.",
};
}
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) {
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete" || command.kind === "findOneAndUpdate" || command.kind === "findOneAndReplace" || command.kind === "findOneAndDelete") && isEmptyJsonObject(command.filter)) {
return {
allowed: false,
reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.",