Auditing CVEs in vendored GDAL and PROJ

This page answers one question: an advisory lands against libtiff, libcurl or GDAL itself, and your wheels bundle all three — so how do you determine within minutes whether your published artifacts are affected, and what do you ship if they are? It sits inside the Security Boundaries and Sandboxing section of the Geospatial C-Extension Fundamentals & ABI Architecture reference, and gives you the inventory, the matching procedure and the response.

Why a vendored wheel changes who has to respond to an advisory Two paths from an advisory to a fixed user. With a system library, the distribution ships a patched package and the user updates; the package maintainer does nothing. With a vendored library, the advisory reaches the maintainer, who must rebuild every platform wheel against the patched source, publish a release and notify users, who must then upgrade. The user cannot fix it themselves at any point. advisory published system library: the distribution patches it the user runs an ordinary update — done vendored: you check your inventory rebuild every platform wheel publish and tell people users upgrade — pins stay vulnerable

Context & Root Cause

The vendoring decision transfers patch responsibility from the operating system to the package maintainer, and does so silently — nothing in the build announces it. A self-contained GDAL wheel typically carries a dozen third-party libraries, several of which are format parsers with long advisory histories: libtiff, libwebp, libjpeg, libpng, libcurl, openssl, sqlite3, libexpat. Any of them can be the subject of an advisory, and a user running your wheel is running your copy regardless of what their system has.

The reason this becomes urgent rather than theoretical is the threat model: those parsers exist to read files supplied by whoever produced the data, and a geospatial pipeline is in the business of reading files from elsewhere. The security boundaries and sandboxing chapter treats that as the central threat; this page is the operational half — knowing, quickly, whether a given advisory reaches your users.

Solution / Fix

This targets a vendored wheel built per vendoring PROJ and GDAL vs system libraries, with auditwheel 6.x and ordinary shell tooling.

1. Produce an inventory from the artifact, not the manifest

unzip -o dist/*.whl -d /tmp/w >/dev/null
for so in /tmp/w/*.libs/*.so*; do
  base=$(basename "$so" | sed -E 's/-[0-9a-f]{6,}//')
  ver=$(strings "$so" | grep -ioE "(libtiff|libwebp|libcurl|sqlite|openssl|proj|gdal|geos)[ /-]?[0-9]+\.[0-9]+(\.[0-9]+)?" | head -1)
  printf '%s\t%s\n' "$base" "${ver:-unknown}"
done | sort -u | tee dist/inventory.tsv

2. Record it where a user can read it without unpacking anything

# geo_core/_vendored.py — generated at build time, shipped in the wheel
VENDORED = {
    "gdal": "3.8.4", "proj": "9.3.1", "geos": "3.12.1",
    "libtiff": "4.6.0", "libwebp": "1.3.2", "sqlite3": "3.45.1",
    "libcurl": "8.6.0", "zlib": "1.3.1",
}

3. Match an advisory against it

# Given an advisory affecting libtiff below 4.6.1
awk -F'\t' '$1 ~ /libtiff/ {print}' dist/inventory.tsv
# 4.6.0 → affected; rebuild required

4. Ship the rebuild as an ordinary patch release

Bump only the vendored version, rebuild the full platform matrix, run the validation gate, and publish. The package version changes because the artifact changed, even though no line of your own code did.

Verification

# 1. Every bundled library is in the inventory — nothing untracked
diff <(ls /tmp/w/*.libs/ | sed -E 's/-[0-9a-f]{6,}//; s/\.so.*//' | sort -u) \
     <(cut -f1 dist/inventory.tsv | sed -E 's/\.so.*//' | sort -u)
# expected: no differences
# 2. The runtime inventory matches the artifact inventory
python -c "from geo_core._vendored import VENDORED; print(VENDORED['libtiff'])"
# expected: the same version the artifact scan reported
# 3. After a security rebuild, the version actually moved
python -c "from geo_core._vendored import VENDORED; assert VENDORED['libtiff'] >= '4.6.1'"

The first check is the one that keeps the inventory honest over time. A library added by a GDAL configure change — a new codec pulled in by a driver — appears in the wheel without appearing in a hand-maintained list, and the diff catches it on the next build rather than during an incident.

Deciding Whether an Advisory Reaches Your Users

Not every advisory against a bundled library affects your wheel, and rebuilding for every one is unsustainable. Three questions narrow it quickly.

Three questions that decide whether an advisory affects your wheel First, is the library actually bundled and at an affected version — the inventory answers this directly. Second, is the vulnerable code path reachable, which depends on whether the relevant driver or feature was compiled in. Third, does the wheel's usage expose it, which depends on whether the affected input format is one your users parse. A yes to all three means rebuild now; a no to the first means no action; ambiguity in the second or third means rebuild anyway on the next release. 1 · is it bundled? check the inventory 2 · is the code reachable? was that driver compiled in? 3 · do users hit it? is that format parsed? no at step 1 no action; record the check unclear at 2 or 3 rebuild on the next release yes at all three rebuild and publish now step two is answerable from the build configuration: a driver you disabled cannot reach the vulnerable parser which is a second, quieter argument for pruning the driver set

Step two is where a narrowed driver set pays a dividend that has nothing to do with size. If your GDAL was configured without the JPEG2000 drivers, an advisory in a JPEG2000 codec cannot be reached through your wheel even if the library is technically present — and if the codec was never linked at all, it is not present either. Recording the enabled driver set alongside the version inventory makes that determination a lookup rather than an investigation.

Be careful with step three, though. “Our users only read GeoTIFF” is a statement about intent, not about what the code does: GDAL probes formats by content, so a file claiming to be one format can reach a different driver. Treat step three as a reason to deprioritise rather than to dismiss.

Responding Without Panic

A security rebuild is an ordinary release with a shorter timeline, and having the steps written down in advance is most of what makes it calm.

The security-rebuild sequence, from advisory to notified users Five steps in order. Bump the pinned version of the affected library in the single file that records native versions. Rebuild the full platform matrix, which the cache makes fast for everything except the changed library. Run the ordinary validation gate, unchanged. Publish as a patch release. Then notify: release notes naming the advisory, and a note in the repository. The whole sequence is the normal release process with one input changed. bump the pin one line rebuild matrix cache covers the rest validate unchanged gate publish patch release notify notes + issue nothing here is special-cased — it is the ordinary release path with one input changed which is exactly why the pinned versions should live in one file rather than in five and why the release notes template should already have a line for vendored versions

The notification step is the one most often skipped and the one users value most. A release note that names the advisory, says which vendored library moved and from which version to which, tells every downstream reader whether they need to act — and does so without them having to ask.

Pitfalls & Alternatives

Relying on a scanner that reads Python metadata. It sees your declared dependencies and none of the native payload, so it reports a clean bill of health for a wheel bundling a vulnerable parser. The inventory has to come from the artifact.

Assuming the advisory’s fixed version is available. Sometimes the patched release of a library breaks another one in the stack. Rebuilding is still the right response; be prepared for the fix to require bumping a second dependency as well, and give the validation gate room to catch it.

Yanking the affected release. Tempting, and usually wrong: yanking removes the version from ordinary resolution but leaves pinned users on it, and it removes a version people may need while they test the upgrade. Publish the fix and say clearly which versions are affected.

Letting the inventory be hand-maintained. It drifts within two releases, and it drifts in the direction of understating what you ship. Generate it from the built wheel every time, and diff it against the previous release as part of the ordinary pipeline.

Frequently Asked Questions

How quickly should a security rebuild ship?

Faster than an ordinary release and not so fast that the validation gate is skipped. For a parser advisory with a public exploit, same-week is a reasonable target; for a theoretical issue in a code path your build does not enable, the next scheduled release is fine. What matters is that the decision is made from the inventory rather than from a headline.

Should the advisory be mentioned in the release notes?

Yes, with the library, the versions before and after, and a one-line statement of whether your build could reach the affected code. Users who track advisories are looking for exactly that paragraph, and its absence generates issues that cost more time than writing it.

Does a private index change anything?

Only the distribution step. The inventory, the matching and the rebuild are identical; what differs is that you can often reach every consumer directly, which makes notification easier than it is on a public index.

What about advisories in the Python layer rather than the native one?

Ordinary dependency tooling handles those, because they are declared dependencies a resolver can see. The native inventory exists precisely because that tooling cannot see inside the wheel — the two are complementary rather than alternatives.

How do I keep the inventory accurate as GDAL’s configure changes?

Generate it from the built wheel on every release and diff it against the previous one. A driver enabled by a configure change can pull in a codec nobody added deliberately, and the diff surfaces that as a new line rather than as a surprise during an incident.

Should older releases be rebuilt as well?

Rarely, and only for a supported maintenance line. Rebuilding a two-year-old release against a patched library means rebuilding against a toolchain that has also moved, which produces an artifact different enough to need its own validation. Publishing a current fix and stating clearly which versions are affected serves users better.

Who should own this in a team?

Whoever owns releases, because the response is a release. Assigning it to a security function that cannot cut one produces a hand-off at exactly the moment speed matters. What the security function can usefully own is the watching — subscribing to the feeds for the libraries in the inventory and raising the question — while the release owner keeps the inventory current and does the rebuild. Keeping the watching and the rebuilding in the same team, or at least in the same weekly conversation, is what keeps the gap between advisory and release measured in days rather than in quarters.