fix(mongodb): validate options and bound findOne queries
This commit is contained in:
1 parent
d098e3e970
commit
55f15c8e4b
14 files changed
+934
-5
No files matched your search
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user