Conversation
…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.
|
I'll analyze this and get back to you. |
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" |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
| 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)" |
There was a problem hiding this comment.
⚪ [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.
|
dotbot code review:
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.

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.docker/java-base/build-libvips.sh— builds libvips 8.18.6 (SHA-256 pinned; same 8.18 line the shippedvips-ffm1.9.8 bindings were generated from, samelibvips.so.42soname) with-Dmagick=disabled -Dopenexr=disabledinto/usr/local/libvips. It also emitsruntime-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'sapt autoremovecannot gut it.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 installinglibvips42/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.dotcms/java-baseimage instead of apt-installing Ubuntu'slibvips42, soVipsParityTestexercises the configuration that actually ships.DOT_TEST_REQUIRE_LIBVIPS=trueturns 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.yml—ignore-unfixed: false(rationale below).Checklist
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 upgraderuns at build time)". That is not the case on Ubuntu 24.04:noble-securityandnoble-updatesare both enabled and contribute no candidate, so no rebuild clears these. They reach the image only as hardDependsoflibvips42t64, and removing those dependencies with apt removes libvips itself — apt cascades to the dependent package. OpenEXR is worse: it is statically linked intolibvips.so.42(lddreportslibOpenEXR-3_1.so.30), so deleting that library would breaklibvips.so.42outright 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 inoriginal/Dockerfileandvips/README.md. It is not:dotmarketing-config.properties:949sets ittrue, andVipsManager.isEnabled()isConfig.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: falseis 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):
dotcms/java-baseverify-libvips: PASS (59 checks)dotcms/dotcmsPASS (59 checks)— also re-run under the non-root runtime user (uid 65001)dotcms/dotcms-devPASS (59 checks)In the built runtime image:
libmagickcore-6.q16-7t64,libopenexr-3-1-30,imagemagick-6-common,libvips42t64are all absent from the package database; nolibMagick*/libOpenEXR*/libImath*anywhere on the filesystem;vips-heif.so/vips-jxl.so/vips-poppler.so/vips-openslide.sopresent with novips-magick.so; AVIF encodes and decodes.VipsParityTest: 22 tests, 0 failures, 0 skipped.On the test change:
animated_gif_resize_preserves_animationpreviously used the bundledtest.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 bothtransform()and therunFilter()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: 0and exited 0 (the silent-zero-coverage failure mode); withDOT_TEST_REQUIRE_LIBVIPS=trueit exits 1 withnative libvips not available on this host (TEST_REQUIRE_LIBVIPS is set, so this is a failure).Accepted format losses
IMAGE_API_LIBVIPS_FALLBACK=truecovers the middle row automatically but cannot recover the last row — those formats have no JVM decoder either.EPS/PSwere already unusable (Ghostscript is not installed) and are unchanged.Merge order
dotcms/java-basemust be rebuilt and pushed viacicd_manual_build-java-base.ymlbefore the CI change here can go green, since the new extraction step pullsdotcms/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 existingimage_suffixinput is the escape hatch.