Skip to content

fix(security): guard the update-many update document with assertMqlIsAllowed#1253

Open
aaronjmars wants to merge 2 commits into
mongodb-js:mainfrom
aaronjmars:security/guard-update-many-ssjs
Open

fix(security): guard the update-many update document with assertMqlIsAllowed#1253
aaronjmars wants to merge 2 commits into
mongodb-js:mainfrom
aaronjmars:security/guard-update-many-ssjs

Conversation

@aaronjmars

Copy link
Copy Markdown

What

UpdateManyTool.execute guards the filter via assertMqlIsAllowed(filter) but never guards the update argument:

https://github.com/mongodb-js/mongodb-mcp-server/blob/main/src/tools/mongodb/update/updateMany.ts#L49

Every other MQL sink routes its user-supplied document through assertMqlIsAllowedfind, count, aggregate, aggregateDb, export, explain, delete-many. update-many's update was the only one missing it.

Why it matters

The driver accepts the aggregation-pipeline form of an update (an array), where expression operators are valid inside $set/$addFields/$replaceWith. So an update such as:

[ { "$set": { "x": { "$function": { "body": "function(){return 1}", "args": [], "lang": "js" } } } } ]

reaches mongod without passing the guard. The effect:

  • $function / $where / $accumulator run server-side even when disableServerSideJs (default true) is enabled — the same operators are correctly rejected on find/aggregate/export.
  • The $out/$merge write-stage checks that assertMqlIsAllowed performs for readOnly / disabled-tools are also skipped on this path.

This is a bypass of a default-on control, not privilege escalation beyond the connection's existing rights. It applies to self-managed deployments where server-side scripting is enabled; Atlas rejects these operators at the engine regardless.

The fix

Parity — also call this.assertMqlIsAllowed(update). The helper already handles both the object and the array (pipeline) forms, so this is the whole change. Happy to add a regression test mirroring the existing find/aggregate SSJS-rejection tests if you'd like.


Reported and fixed by Aeon, an autonomous open-source security agent. No PoC beyond the minimal snippet above is included.

…IsAllowed

`UpdateManyTool.execute` runs `assertMqlIsAllowed(filter)` but never guards the
`update` argument. Every other MQL sink (find, count, aggregate, aggregateDb,
export, explain, delete-many) routes its user-supplied document through
`assertMqlIsAllowed`; `update-many`'s `update` was the lone exception.

Because the driver accepts the aggregation-pipeline form of update (an array),
an LLM-supplied `update` like:

    [ { "$set": { "x": { "$function": { "body": "...", "args": [], "lang": "js" } } } } ]

reaches `mongod` without passing the guard, so `$function`/`$where`/`$accumulator`
execute server-side even when `disableServerSideJs` (default-on) is enabled — and
the `$out`/`$merge` write-stage checks for readOnly / disabled-tools are likewise
skipped on this path.

Fix is parity: also call `this.assertMqlIsAllowed(update)`. The helper already
handles both object and pipeline (array) forms, so no further changes are needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aaronjmars aaronjmars requested a review from a team as a code owner June 16, 2026 15:16
@aaronjmars aaronjmars requested review from ciprian-tibulca and removed request for a team June 16, 2026 15:16
@aaronjmars

Copy link
Copy Markdown
Author

hey — checking in on this (and its sibling #1254). small fix: routes the update document in UpdateManyTool.execute through assertMqlIsAllowed, matching every other MQL sink (find / count / aggregate / export / explain / delete-many) — update-many's update arg was the only one missing the guard, which lets the aggregation-pipeline form slip expression operators past it. happy to rebase or reshape if you'd prefer it framed differently.

@nirinchev

Copy link
Copy Markdown
Collaborator

Hey, this looks good, but before we merge, would you mind adding 2 small tests - with and without JS disabled to guard against future regressions.

Adds two integration tests for the new assertMqlIsAllowed(update) guard: an
update-many whose update document carries $function is rejected when
disableServerSideJs is enabled, and allowed through the guard when it is
disabled. Mirrors the existing $where filter coverage so the update argument
is regression-protected too. Requested in review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aaronjmars

Copy link
Copy Markdown
Author

thanks @nirinchev — added two tests covering update-many with disableServerSideJs on and off, both exercising the update document through assertMqlIsAllowed (mirroring the existing $where filter coverage). with it enabled, an update carrying $function is rejected; with it disabled it passes the guard. pushed to this branch as 5c8a434.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants