Preventing WKT/WKB Injection in Spatial Queries

A geometry payload is untrusted input, and treating a WKT or WKB string as safe because it “looks like coordinates” is how spatial injection happens. This guide sits beneath security boundaries in spatial QA and shows how to stop it at the boundary: why concatenating geometry into SQL is exploitable, how to bind geometry as a parameter instead, how to validate untrusted WKT server-side before it reaches a query, and how to gate against resource-exhaustion payloads with size and vertex limits. The QA angle is that these defenses are testable — a suite can prove the injection surface is closed with the same rigor it proves a topology rule.

Why concatenated geometry is exploitable

The vulnerability is identical in shape to classic SQL injection: when a WKT string arrives from a user or an upstream feed and is concatenated into a query, a crafted payload can break out of the geometry literal and append arbitrary SQL. A value like POLYGON((0 0,...))'); DROP TABLE parcels;-- closes the string and injects a statement. Even without a breakout, a hostile geometry can be a resource-exhaustion vector: a WKT with millions of vertices or deeply nested GEOMETRYCOLLECTIONs can exhaust memory in the parser. Both are prevented by never building SQL from geometry text and by bounding what the parser will accept.

Defense reference

Threat Defense Layer
SQL breakout via WKT Parameterized binding, never concatenation Application
Malformed WKT crashing parser ST_GeomFromText in a guarded transaction Database
Vertex-count exhaustion ST_NPoints limit before insert Database
Payload-size exhaustion Byte-length cap on the input Application
Nested collection bomb Reject GEOMETRYCOLLECTION depth Application

Step-by-step implementation

The pattern targets psycopg2, PostGIS 3.x and Shapely 2.x, binding geometry safely and bounding untrusted input.

Step 1 — Bind geometry as a parameter, never concatenate

Pass the WKT as a bound parameter so the driver escapes it; the database parses it with ST_GeomFromText, and a breakout attempt becomes an invalid-geometry error, not executed SQL.

import psycopg2

def insert_parcel(conn, wkt: str, srid: int = 3857):
    with conn.cursor() as cur:
        cur.execute(
            "INSERT INTO parcels (geom) VALUES (ST_GeomFromText(%s, %s))",
            (wkt, srid),          # bound parameters — the driver escapes them
        )

Step 2 — Validate and bound untrusted WKT before it reaches SQL

Parse with Shapely first so unparseable or oversized payloads are rejected at the application boundary, never reaching the database.

from shapely import from_wkt, get_num_points

MAX_BYTES = 1_000_000
MAX_VERTICES = 100_000

def sanitize_wkt(wkt: str) -> str:
    if len(wkt.encode()) > MAX_BYTES:
        raise ValueError("geometry payload too large")
    geom = from_wkt(wkt)                       # raises on malformed WKT
    if get_num_points(geom) > MAX_VERTICES:
        raise ValueError("geometry has too many vertices")
    if geom.geom_type == "GeometryCollection":
        raise ValueError("nested collections not accepted")
    return wkt

Step 3 — Guard the database side too

Enforce the vertex bound in SQL as defense in depth, so a payload that bypasses the application layer is still rejected.

ALTER TABLE parcels ADD CONSTRAINT geom_vertex_cap
  CHECK (ST_NPoints(geom) <= 100000);

Step 4 — Never echo the payload into logs

A rejected payload must be logged by id and reason, never by raw coordinates — both to avoid leaking location PII and to avoid log-injection, the same rule the security boundaries work sets for all untrusted geometry.

Verification pattern

Prove the boundary rejects a breakout payload and an oversized one, as a gate that runs on every push.

import pytest

def test_rejects_sql_breakout():
    with pytest.raises(ValueError):
        sanitize_wkt("POLYGON((0 0,1 0,1 1,0 1,0 0))'); DROP TABLE parcels;--")

def test_rejects_vertex_bomb():
    huge = "LINESTRING(" + ",".join(f"{i} 0" for i in range(200_000)) + ")"
    with pytest.raises(ValueError):
        sanitize_wkt(huge)

A parameterized insert makes the first test’s payload a harmless invalid-geometry string even if it reached the query — but proving the application layer rejects it first is defense in depth.

Failure modes and edge cases

  1. f-string SQL. Building the query with an f-string or % formatting reintroduces the breakout; always pass geometry as a bound parameter.
  2. Trusting WKB because it is binary. WKB is not inherently safe — a malformed or oversized WKB blob still exhausts the parser; apply the same size and vertex bounds.
  3. Validation after the query. Parsing the WKT only after it is inserted is too late; sanitize at the boundary before any SQL runs.
  4. Empty and null bypass. A None or empty payload can slip past a length check and null-poison a predicate; reject non-geometry explicitly.
  5. Coordinates in error messages. Echoing the offending WKT into an exception message leaks location data into logs; report id and reason only.

Conclusion

Preventing WKT/WKB injection is the same discipline as any injection defense applied to geometry: bind it as a parameter, validate and bound it at the application boundary before any SQL runs, enforce a vertex cap in the database as defense in depth, and never echo the payload. Because each defense is testable, a suite can prove the spatial injection surface is closed. For the wider security context, return to security boundaries in spatial QA.