Skip to content

Tidy up tools/, and harden the docs unpacking - #24

Merged
dpage merged 2 commits into
mainfrom
tidy-tools-and-harden-load-docs
Sep 22, 2026
Merged

dpage merged 2 commits into
mainfrom
tidy-tools-and-harden-load-docs

Conversation

@dpage

@dpage dpage commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Now that publishing runs through pkg/publish/pga-publish in pgadmin4 rather than Jenkins, tools/ carries a few scripts that either have a better home or are outright duplicates, and load-docs.sh sits at the point where the wrapper's containment ends.

Merge ordering, please read first

sync-ftp-to-s3.py and purge_pgadmin_releases.sh are being moved into pgadmin4 under pkg/publish/ by work happening right now. This PR must not be merged until that change has landed and the new wrapper has been installed on the download server, because the wrapper finds the S3 sync by absolute path under /var/www/pgaweb/tools and removing it early breaks the sync-s3 verb.

The other two commits have no such constraint, so if the timing is awkward the removals commit can be split.

What changed

Deleted rebuild-apt-repo.sh and rebuild-yum-repo.sh, which are superseded by the parameterised versions the wrapper actually runs at pkg/publish/ in pgadmin4, and which nothing here called.

Hardened load-docs.sh. The docs tarball is assembled by a CI runner and written into a tree the upload key can reach, and the script unpacked it straight over /var/www/pgaweb/static/docs, unquoted and unchecked, so the archive rather than the wrapper chose where the files landed. It now validates the shape of the version argument before interpolating it into a path, vets the member list before unpacking anything (plain files and directories only, all under pgadmin4-<version>-docs), unpacks into a staging directory with --no-same-owner --no-same-permissions, and moves the result into place only once it looks like the docs that were asked for. A good tarball behaves as before, aside from losing tar's verbose file listing.

Corrected tools/README.md, which described four scripts as being called by Jenkins.

For a reviewer to decide

The member vetting requires a single pgadmin4-<version>-docs top level directory, which is what the published tarballs have carried and what the docloader call has always assumed. .github/workflows/release-candidate.yml in pgadmin4 builds the tarball with tar -czf ... -C artefacts/pgadmin4-html-build-output ., which would produce a flat archive instead; if that is what the first Actions release actually uploads, this script will refuse it rather than silently loading nothing, but the packaging step is the thing that wants fixing. Worth confirming against the next release candidate before it is a release.

Testing

Verified against a real published tarball (pgadmin4-9.18-docs.tar.gz), and against crafted archives containing an absolute path member, ../ traversal members, a symlink pointing outside the tree with a later member written through it, a flat archive with no top level directory, a second top level directory, and a setuid member. All four hostile cases are refused with nothing written outside the tree and nothing left behind, a hostile tarball arriving on top of good docs leaves the existing docs in place, and the setuid bit is dropped.

The docs tarball is assembled by a CI runner and written into a tree that
the upload key can reach, and the script unpacked it straight over
/var/www/pgaweb/static/docs with an unquoted path and no checks at all, so
the archive rather than the wrapper decided where those files landed. That
is where the publishing wrapper's containment ended.

The version argument is now checked for shape before it is interpolated
into either the tarball path or the directory name, every variable is
quoted, and the archive is vetted before anything is unpacked: every member
must be a plain file or a directory, which rejects symlinks and hard links
that would redirect a later member out of the tree, and every member must
sit under the pgadmin4-<version>-docs directory, which rejects absolute
paths and '..' traversal. Unpacking then happens into a staging directory
alongside the live one, with --no-same-owner and --no-same-permissions, and
the result is checked and moved into place only once it looks like the docs
we asked for, so a bad tarball can no longer leave the site serving a half
replaced tree.

The comments explain what each check is for, since the next reader will
otherwise have no way of knowing. Behaviour on a good tarball is unchanged
apart from the loss of tar's verbose file listing, with the docloader call
and the cache ban exactly as they were.
rebuild-apt-repo.sh and rebuild-yum-repo.sh are superseded by the
parameterised versions at pkg/publish/ in pgadmin4, which are what the
publishing wrapper actually runs; nothing here called them, and two copies
of the repository signing logic is precisely the duplication the move to
GitHub Actions set out to remove.

sync-ftp-to-s3.py and purge_pgadmin_releases.sh operate on the download
tree at /var/ftp/pgadmin4 and touch nothing in this Django application, so
they belong beside the rest of the publishing machinery rather than here.

The README described four scripts as being called by the Jenkins server,
which is no longer true of any of them. The release time ones are now
reached through the publishing wrapper from GitHub Actions, whilst
update-docs.sh runs from cron on the web server and never had anything to
do with Jenkins. The note about the AWS CLI goes with sync-ftp-to-s3.py,
which was the only thing that used it.
@dpage
dpage merged commit 62249be into main Sep 22, 2026
5 checks passed
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