Skip to content

fix(image): build libvips without the ImageMagick and OpenEXR delegates - #37591

Open
wezell wants to merge 7 commits into
mainfrom
issue-36546-image-libvips-delegates
Open

wezell wants to merge 7 commits into
mainfrom
issue-36546-image-libvips-delegates

Conversation

@wezell

@wezell wezell commented Sep 17, 2026

Copy link
Copy Markdown
Member

Proposed Changes

Removes the flagged ImageMagick/OpenEXR packages from the dotCMS Docker images by building libvips from source without those delegates.

Refs #36546 — this covers only the OS-layer items from that issue. Its stated scope (netty, commons-fileupload, grpc bumps in bom/application/pom.xml) is untouched, so this does not close it.

  • New docker/java-base/build-libvips.sh — builds libvips 8.18.6 (SHA-256 pinned; same 8.18 line the shipped vips-ffm 1.9.8 bindings were generated from, same libvips.so.42 soname) with -Dmagick=disabled -Dopenexr=disabled into /usr/local/libvips. It also emits runtime-packages.txt, the apt closure derived from the resolved ELF dependencies of the library, CLI and every dynamic module — plus the dlopened libheif codec plugins, which ELF inspection cannot see — and marks it manual so the image's apt autoremove cannot gut it.
  • New docker/java-base/verify-libvips.sh — a 59-check acceptance gate: forbidden packages/libraries absent, no MagickCore/OpenEXR linkage, no unresolved dependencies, required delegates present, and real encode/decode round trips (AVIF, JPEG2000, JXL, WebP, TIFF, GIF, PNG, JPEG, SVG).
  • docker/java-base/Dockerfile — builds it once and reuses it downstream, mirroring the existing tcnative pattern.
  • dotCMS/src/main/docker/original/Dockerfile — stops installing libvips42/libheif-plugin-aomenc, copies the prefix, installs the manifest, and runs the gate after all apt operations.
  • docker/dev-env/Dockerfile — same treatment. dev-env previously shipped no libvips at all and silently ran the legacy JVM engine; it now matches production.
  • CI — the JVM job extracts libvips from the published dotcms/java-base image instead of apt-installing Ubuntu's libvips42, so VipsParityTest exercises the configuration that actually ships. DOT_TEST_REQUIRE_LIBVIPS=true turns a missing engine into a failure rather than a skip.
  • VipsParityTest — adds AVIF-decode coverage and real GIF frame-count assertions.
  • cicd_scheduled_image-cve-scan.ymlignore-unfixed: false (rationale below).
  • Docs — corrects two false "the engine is off by default" claims.

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

Why a version bump was not an option

#36546 classifies these packages as "patched automatically on image rebuild once Canonical publishes fixed packages (apt upgrade runs at build time)". That is not the case on Ubuntu 24.04:

libmagickcore-6.q16-7t64  Candidate: 8:6.9.12.98+dfsg1-5.2build2
    500 http://ports.ubuntu.com/ubuntu-ports noble/universe    <- only pocket, only version
libopenexr-3-1-30         Candidate: 3.1.5-5.1build3
    500 http://ports.ubuntu.com/ubuntu-ports noble/universe    <- only pocket, only version

noble-security and noble-updates are both enabled and contribute no candidate, so no rebuild clears these. They reach the image only as hard Depends of libvips42t64, and removing those dependencies with apt removes libvips itself — apt cascades to the dependent package. OpenEXR is worse: it is statically linked into libvips.so.42 (ldd reports libOpenEXR-3_1.so.30), so deleting that library would break libvips.so.42 outright and dotCMS would silently fall back to the pure-JVM engine.

The engine is not off by default

#36546 also states "the libvips engine is off by default (IMAGE_API_USE_LIBVIPS)", echoing comments in original/Dockerfile and vips/README.md. It is not: dotmarketing-config.properties:949 sets it true, and VipsManager.isEnabled() is Config.getBooleanProperty(USE_LIBVIPS, true) && isAvailable(). The engine activates in the shipped image, so the ImageMagick delegate was reachable through uploaded images rather than being unreachable dead weight. Both stale comments are corrected in this PR.

This also explains why ignore-unfixed: false is part of the change: the scheduled scan could not surface any of this, because unfixed findings were being suppressed and these packages have no fix.

Verification

Built and validated locally on arm64 (the multi-arch workflow also builds amd64, under QEMU):

Image Acceptance gate
dotcms/java-base verify-libvips: PASS (59 checks)
dotcms/dotcms PASS (59 checks) — also re-run under the non-root runtime user (uid 65001)
dotcms/dotcms-dev PASS (59 checks)

In the built runtime image: libmagickcore-6.q16-7t64, libopenexr-3-1-30, imagemagick-6-common, libvips42t64 are all absent from the package database; no libMagick*/libOpenEXR*/libImath* anywhere on the filesystem; vips-heif.so/vips-jxl.so/vips-poppler.so/vips-openslide.so present with no vips-magick.so; AVIF encodes and decodes. VipsParityTest: 22 tests, 0 failures, 0 skipped.

On the test change: animated_gif_resize_preserves_animation previously used the bundled test.gif, which is single-frame (n-pages=1), so it could not distinguish a preserved animation from one flattened to a single frame. It now builds a genuinely animated input and asserts the frame count survives in both transform() and the runFilter() production path — both pass, confirming animation is preserved.

Strict mode was verified in both directions: with libvips absent and the flag unset the suite reported Tests run: 0 and exited 0 (the silent-zero-coverage failure mode); with DOT_TEST_REQUIRE_LIBVIPS=true it exits 1 with native libvips not available on this host (TEST_REQUIRE_LIBVIPS is set, so this is a failure).

Accepted format losses

Result Formats
Native libvips (unchanged) AVIF/HEIC, WebP, JXL, JPEG2000, TIFF, PNG (spng), JPEG, animated GIF, PDF (poppler), SVG (librsvg), PNM, FITS, Matlab, OpenSlide
Now served by the JVM fallback PSD, BMP, PCX, TGA, PICT, SGI, IFF, ICNS, HDR
No longer supported ICO, DDS, DPX, XCF, and EXR in any form

IMAGE_API_LIBVIPS_FALLBACK=true covers the middle row automatically but cannot recover the last row — those formats have no JVM decoder either. EPS/PS were already unusable (Ghostscript is not installed) and are unchanged.

Merge order

dotcms/java-base must be rebuilt and pushed via cicd_manual_build-java-base.yml before the CI change here can go green, since the new extraction step pulls dotcms/java-base:<java-version> from Docker Hub; the runtime and dev-env images build from the same image. The tag scheme is deliberately unchanged (no new build argument) — if a libvips-only rebuild is ever needed without a Java version bump, the workflow's existing image_suffix input is the escape hatch.

…moved

Ubuntu 24.04's libvips42t64 hard-depends on libmagickcore-6.q16-7t64 and
libopenexr-3-1-30. Those two packages account for every ImageMagick and OpenEXR
CVE in the security report (helpdesk ticket 38243 / #36546), and noble ships a
single version of each with no security-pocket candidate - so a rebuild cannot
clear them.

Build libvips from source instead, pinned to 8.18.6 (the version the shipped
vips-ffm 1.9.8 bindings were generated from, and still soname libvips.so.42)
with -Dmagick=disabled -Dopenexr=disabled. Everything dotCMS actually uses -
AVIF/HEIC via libheif, PDF via poppler, SVG via librsvg, animated GIF via
cgi/nsgif, WebP, JXL, JPEG2000, TIFF, PNG via spng, JPEG - is retained.

- build-libvips.sh: checksum-verified source build into /usr/local/libvips,
  writes an apt manifest of the runtime closure derived from resolved ELF
  dependencies (plus the dlopened libheif codec plugins, which ELF inspection
  cannot see), marks it manual so a later autoremove cannot gut it, records
  provenance, and runs the acceptance gate.
- verify-libvips.sh: 59 checks - forbidden packages and libraries absent, no
  MagickCore/OpenEXR linkage, no unresolved deps, required delegates present,
  and real encode/decode round trips including AVIF and JPEG2000.
- Dockerfile: runs the build after the existing cleanup so the gate verifies
  the image after every apt operation. The stale PGDG source is dropped first:
  an earlier step purges gnupg, leaving that repo unverifiable, and apt update
  therefore aborts with NO_PUBKEY once the index lists are gone.
Stop installing libvips42 (and libheif-plugin-aomenc as a separate apt line) in
the runtime image, which is what pulled libmagickcore-6.q16-7t64,
imagemagick-6-common and libopenexr-3-1-30 into every dotCMS image.

- Copy /usr/local/libvips from java-base, where it is built once (the same
  pattern already used for tcnative), plus the ld.so.conf.d entry that makes
  libvips.so.42 resolvable.
- Install the runtime closure from the manifest generated at build time. apt
  cannot resolve it from libvips42 any more, so it must be explicit.
- Run verify-libvips.sh after all apt operations. A wrong module path does not
  raise an error - libvips silently falls back to the pure-JVM engine and
  VipsParityTest self-skips - so this has to be an explicit assertion.
- Fix the comment claiming the engine is 'off by default'. It is not:
  IMAGE_API_USE_LIBVIPS defaults to true and activates whenever the native
  library is loadable, which it is in this image.

dev-env gets the same treatment. It previously shipped no libvips at all, so it
silently ran the legacy JVM engine; it now matches production.
- VipsParityTest job: extract libvips from the published dotcms/java-base image
  rather than apt-installing Ubuntu's libvips42, so the parity tests exercise
  the native configuration that actually ships. Set DOT_TEST_REQUIRE_LIBVIPS so
  a missing engine fails instead of skipping, and run verify-libvips.sh to fail
  before the tests even start when the native build is broken.
- Scheduled image scan: ignore-unfixed: false. Ubuntu ships exactly one version
  of several vulnerable packages with no patched candidate, so ignoring unfixed
  findings is why this scan never surfaced the magickcore/openexr exposure.
…verage gaps

- Add TEST_REQUIRE_LIBVIPS (env DOT_TEST_REQUIRE_LIBVIPS), read through Config so
  it uses the standard DOT_ prefix. In strict mode a missing native engine, PDF
  delegate or AVIF encoder fails instead of skipping; outside it the previous
  skip behaviour is kept for local runs. Verified: without libvips the suite
  reported 'Tests run: 0' and exited 0, and with the flag set it fails with an
  explicit message.
- animated_gif_resize_preserves_animation used the bundled test.gif, which is
  single-frame (n-pages=1), so it could not tell a preserved animation from one
  flattened to a single frame. Build a genuinely animated input and assert the
  frame count survives, in both transform() and the runFilter() production path.
  Both pass against libvips 8.18, confirming animation is preserved.
- Add an AVIF decode assertion: reading the encoded output back through heifload
  catches a write-only bug that a container-magic check would miss (dotCMS has
  no Java filter for JXL/JPEG2000, so those are covered by verify-libvips.sh).
- README: correct the claim that the legacy engine 'remains the default' and
  that an unprefixed env var pins the engine to legacy (it keeps the true
  default), and document which formats are native, which fall back to the JVM
  engine, and which are no longer supported.
@claude

claude Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

I'll analyze this and get back to you.

View job run

The gate failed 4 of 59 checks in CI: imagemagick-6-common and
libmagickcore-6.q16-7t64 'installed', plus libMagickCore/libMagickWand present
under /usr/lib. None of that had anything to do with the image - the GitHub
ubuntu-24.04 runner ships ImageMagick preinstalled. The host-wide assertions only
make sense inside the images we build, where their absence is the point.

Add VIPS_VERIFY_SCOPE:
- image (default, used by the Dockerfiles): unchanged - also asserts the host
  carries none of the forbidden packages or libraries.
- prefix (used by the CI step, which only has an extracted prefix): asserts
  version, soname, no MagickCore/OpenEXR linkage, delegates, operations and
  encode/decode round trips, and prints why the host checks are skipped.

Verified against the real prefix on a host with ImageMagick installed:
scope=prefix passes 48 checks; scope=image still fails 5, so the image gate is
not weakened.

The CI step also runs the verifier from the checkout instead of the copy
embedded in the published image, so a verifier fix no longer requires
republishing java-base before it can take effect.

FORBIDDEN_LIB_GLOBS=('libMagickCore*' 'libMagickWand*' 'libOpenEXR*' 'libIlmThread*' 'libImath*')
if [ "$VIPS_VERIFY_SCOPE" != "image" ]; then
printf ' skip host library scan not asserted (scope=%s: the host may legitimately install these)\n' "$VIPS_VERIFY_SCOPE"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified a blocking 🔴 issue in your code:
DNS lookup command uses a variable in the hostname, which could exfiltrate sensitive data via DNS queries. In Claude Code and Cursor hooks, this pattern can leak secrets or environment data to attacker-controlled domains. Use static hostnames only in DNS commands.

Why this might be safe to ignore:

The matched code is a printf statement that reports a verification scope and does not execute a DNS lookup or place data in a hostname. The variable is safely passed as a printf argument, so this is unrelated to DNS-based data exfiltration.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by hooks-dns-exfiltration-generic.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

)

if [ "$VIPS_VERIFY_SCOPE" != "image" ]; then
printf ' skip host package checks not asserted (scope=%s: the host may legitimately install these)\n' "$VIPS_VERIFY_SCOPE"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified a blocking 🔴 issue in your code:
DNS lookup command uses a variable in the hostname, which could exfiltrate sensitive data via DNS queries. In Claude Code and Cursor hooks, this pattern can leak secrets or environment data to attacker-controlled domains. Use static hostnames only in DNS commands.

Why this might be safe to ignore:

The matched code is a printf statement that displays a variable in a diagnostic message; it does not invoke a DNS lookup or place the value in a hostname. This is a false positive from an inaccurate pattern match.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by hooks-dns-exfiltration-generic.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

@wezell wezell added OKR : Security & Privacy Owned by Mehdi Team : Security Issues related to security and privacy PR: docker image Build & push a per-PR test image to dotcms/dotcms-test PR : dotbot review Trigger dotbot AI code review on this PR and removed PR: docker image Build & push a per-PR test image to dotcms/dotcms-test labels Sep 17, 2026
section "ELF linkage"

if [ -d "$VIPS_PREFIX" ] && command -v ldd >/dev/null 2>&1; then
elf_libs="$(find "$VIPS_PREFIX" -type f \( -name '*.so' -o -name '*.so.*' \) 2>/dev/null)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ [P3] verify-libvips.sh:230 include bin/* in ELF linkage scan

Current code:

elf_libs="$(find "$VIPS_PREFIX" -type f \( -name '*.so' -o -name '*.so.*' \) 2>/dev/null)"

Problem: ELF scan omits CLI binaries that manifest includes.

Fix:

elf_libs="$(find "$VIPS_PREFIX" -type f \( -name '*.so' -o -name '*.so.*' \) -o -type f -path "${VIPS_PREFIX}/bin/*" 2>/dev/null)"

…polation

The new step embedded ${{ inputs.java-version }} directly in its run: block.
GitHub substitutes that before the shell parses the line, so it is a script
injection vector even when quoted. Pass it through env as INPUT_JAVA_VERSION and
read the variable instead.

Prompted by the semgrep-cloud-platform/scan check failing on this PR only. That
check publishes no annotations or summary to GitHub (findings live on
semgrep.dev), so this hardens the most likely finding rather than a confirmed
one - worth doing on its own merits either way.
@github-actions

Copy link
Copy Markdown
Contributor

dotbot code review:

  • Reviewer: meta/muse-spark-1.3 (medium)
  • Overall: patch is incorrect
  • New findings this run: 0
  • Prior unresolved dotbot findings still relevant: 1
  • Active findings total: 1

No new actionable bugs were found in the current changes, but 1 prior unresolved dotbot finding still applies, so the patch remains incorrect.

Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads.

reviewed by dotbot · meta/muse-spark-1.3 · medium

Semgrep fails on this PR only, and its mutable-action-tag rule is the standout
candidate: this file (which this PR modifies) runs aquasecurity/trivy-action and
github/codeql-action/upload-sarif from mutable tags, while the same file already
pins actions/upload-artifact to a SHA and the rest of .github/workflows pins
actions with a '# vX.Y.Z' comment.

Pin both to the commits behind their current tags (trivy-action v0.36.0,
codeql-action v4.38.0), which is what that rule asks for and what the repo
already does elsewhere.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code Area : CI/CD PR changes GitHub Actions/workflows OKR : Security & Privacy Owned by Mehdi PR : dotbot review Trigger dotbot AI code review on this PR Team : Security Issues related to security and privacy

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant