Skip to content

Keep uploaded shapefile names and projections out of the shell and SQL - #64

Merged
njakobsen merged 1 commit into
masterfrom
shell-injection-fix
Aug 16, 2026
Merged

Keep uploaded shapefile names and projections out of the shell and SQL#64
njakobsen merged 1 commit into
masterfrom
shell-injection-fix

Conversation

@njakobsen

Copy link
Copy Markdown
Member

Addresses the privately reported vulnerability tracked in GHSA-jfrj-r728-qcc9, reported by @saidM. Releases as 3.11.2.

Importers::Shapefile built shell command strings for gdalsrsinfo and ogr2ogr out of the extracted .shp path, and Unzip.extract writes each archive entry under whatever name that entry carries, so a ZIP whose entry name held shell metacharacters ran as a command. The same class interpolated the PROJ.4 string read from the archive's .prj into both the ogr2ogr command and the ST_Transform statement, so a crafted projection reached the shell and the database as well. All of it runs on the ordinary import path before the shapefile is validated, and a host app cannot defend against it because the gem owns the unzip-then-shell-out step.

Those values are now passed to gdalsrsinfo and ogr2ogr as an argv list so no shell parses them, and the projection is quoted with connection.quote before it reaches the statement. Importers::ESRIGeoJSON gets the same treatment. Unzip.extract additionally resolves each entry's destination against the real path of the directory it extracts into and skips entries that land outside it, along with symlink entries.

Note the affected range is <= 3.11.1, not <= 3.11.0 as originally filed — 3.11.1 is published and carries the same code.

Verification

Three regression specs, each confirmed to fail against the unpatched code before passing against the fix. Every guard in unzip.rb and every quoting and argv change was mutation-tested by removing it and confirming a spec goes red — nine in total, all covered.

Full suite green on both CI legs locally: 279 examples, 0 failures on Ruby 3.2 / Rails 7 and on Ruby 4.0 / Rails 8.

🤖 Generated with Claude Code

https://claude.ai/code/session_017uHh49RG66T3qNFNRnPY6s

…nd SQL

`Importers::Shapefile` built shell command strings for `gdalsrsinfo` and `ogr2ogr` out of the extracted `.shp` path, and `Unzip.extract` writes each archive entry under whatever name that entry carries, so a ZIP whose entry name held shell metacharacters ran as a command. The same class interpolated the PROJ.4 string `gdalsrsinfo` reads from the archive's `.prj` into both the `ogr2ogr` command and the `ST_Transform` statement in `data_from_record`, so a crafted projection reached the shell and the database as well. All of it runs on the ordinary import path before the shapefile is validated, and a host app cannot defend against it because the gem owns the unzip-then-shell-out step.

Instead of interpolating those values into a command string, we pass them to `gdalsrsinfo` and `ogr2ogr` as an argv list so no shell parses them, and quote the projection with `connection.quote` before it reaches the statement. `Importers::ESRIGeoJSON` gets the same argv treatment for its own `ogr2ogr` call. `proj4_from_file` rescues `Errno::ENOENT` so a missing `gdalsrsinfo` still surfaces as `IndeterminateShapefileProjection`, which is what the string form gave us by returning an empty string.

`Unzip.extract` now resolves each entry's destination against the real path of the directory it extracts into, skipping entries that land outside it along with symlink entries. Neither escape was reachable before, since `IGNORED_ENTRY_PATHS` turns away every `..` name and rubyzip declines to create symlinks on extract, so this makes containment a property of the method itself rather than of a pattern written to hide dotfiles and of a dependency's behaviour.

Reported by @saidM.

Refs GHSA-jfrj-r728-qcc9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njakobsen
njakobsen merged commit a90c19f into master Aug 16, 2026
2 checks passed
@njakobsen
njakobsen deleted the shell-injection-fix branch August 17, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant