Skip to content

Harden filter endpoints against MongoDB operator keys ($where server-side JS) #289

Description

@thehabes

Split out of the static review of #286. Filed as hardening rather than an incident — see "Scope" below, this is long standing and API wide, not something #286 introduced.

Summary

Endpoints that accept a JSON object as a MongoDB filter pass $ prefixed keys straight through to the driver. A $ prefixed key is a query operator, not a property name, so $where reaches the server and is evaluated as server side JavaScript. No authentication is required to reach either endpoint.

Affected:

  • POST /v1/api/query — long standing, accepts the whole request body as the filter
  • POST /v1/id/:_id/expanded — added in /v1/id/_:id/expanded endpoint #286; sanitizeExpansionFilters() removes four reserved property names but does not reject operator keys

Impact

Availability. $where predicates are evaluated per candidate document, so request cost scales with the candidate set rather than with the response. A single anonymous request can be made to occupy a connection for a long time, and a small number of concurrent requests would saturate the cluster.

A limited read oracle. A $where predicate has access to the document it is evaluating, and the response reveals whether it matched (result count on /query, the Annotations-Merged header on /expanded). That makes field contents recoverable a character at a time. Slow and noisy, but it means "read only" is not the same as "harmless."

What this is not

Worth stating plainly so this gets weighted correctly:

  • No bypass of the expansion guardrails. Every supplied filter is ANDed into the query, so on /expanded the leaf version, Annotation type, and target constraints hold regardless of what is sent. There is no way to widen the result set past the entity in the request URI.
  • No writes, no authentication or authorization bypass, no access to anything the endpoint would not already return.

Scope

/v1/api/query has accepted operator keys for as long as it has existed, so this is not a regression and /expanded grants an attacker nothing new. It is worth fixing because the fix is cheap and because sanitizeExpansionFilters() currently does not do what its own docblock says it does — it promises to reduce the body to "the literal MongoDB filter keys the expand job will honor," and $where is not a literal filter key.

Suggested fix

1. Disable server side JavaScript on the cluster. This closes $where, $function, $accumulator, and mapReduce everywhere at once, with no code change:

  • Atlas: Configuration → Additional Settings → disable server side JavaScript
  • self hosted: security.javascriptEnabled: false

I grepped the repository for $where, $function, $accumulator, and mapReduce across all .js, .json, .yaml, and .html sources, including the legacy db-controller.js.backup. Zero matches. Nothing in RERUM uses server side JavaScript, so disabling it should be behavior neutral.

2. Reject operator keys at the boundary, as defense in depth. In sanitizeExpansionFilters() (controllers/crud.js) and in query() (controllers/crud.js):

if (key.startsWith("$")) {
    throw Object.assign(
        new Error(`MongoDB operator '${key}' is not an allowed filter. Supply literal property names only.`),
        { status: 400 }
    )
}

Rejecting with a 400 rather than dropping silently means a client that sends one learns why it did not work.

Values need the same treatment, since {"creator": {"$where": "..."}} reaches the same place through a nested object. Either walk values recursively with the same check, or constrain filter values to scalars and arrays of scalars.

Decide deliberately whether /query should keep accepting operators at all. Some operators there are genuinely useful to clients ($exists, $in, $regex) in a way they are not on /expanded, so a blanket $ rejection may be too blunt for /query specifically. An allowlist of safe operators would fit that endpoint better than a denylist. /expanded has no such need and can take the blanket rule.

Notes

Reproduction details are deliberately omitted here since this repository is public and the production instance is live. Available on request through research.computing@slu.edu.

Consider enabling private vulnerability reporting on this repository (Settings → Security) so future findings of this kind have somewhere non public to go.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions