1 Finding to Zero: The One-Line Dockerfile Fix for CVE-2026-46675
Container Security · CVE-2026-46675 · Alpine Remediation
In yesterday’s comparison, one candidate was a single finding away from clean and carried a severity of UNKNOWN. I wrote it off as a CVE nobody had patched yet. That was wrong. The fix had been published for weeks — the image was simply built on an Alpine package snapshot old enough to predate it. One line in the Dockerfile cleared it, and cost 3.1 MB.
1. Where we started
The comparison produced seven candidates. Three of them came back with fewer than five findings, which made them the only images worth spending remediation effort on — you do not spend an afternoon closing a 894-finding gap when a zero-finding image is available.
| Image | Tag | OS findings | App findings | Total | Registry |
|---|---|---|---|---|---|
| Red Hat Hummingbird | cvefix2 | 0 | 0 | 0 | 145.5 MB |
| Amazon Corretto Alpine | cvefix2 | 0 | 0 | 0 | 220.5 MB |
| Eclipse Temurin Alpine | cvefix2 | 1 | 0 | 1 | 132.8 MB |
Two of the three were already at zero and there was nothing left to do. The third was not, and it is the subject of this post.
2. The one finding
Pulled straight out of the Trivy JSON rather than read off a summary table:
The finding carries no severity score because none has been published yet — but it carries a fix version, which is the field that matters.
The title is a use-after-free in png_read_end triggered after incomplete zTXt, iTXt or iCCP decompression. UNKNOWN is not a severity rating of “harmless” — it means nobody has published a CVSS vector yet. On yesterday’s write-up I treated it as a reason to leave the image alone.
The field that settles it. FixedVersion was populated: 1.6.59-r0. A finding with an empty FixedVersion and Status: "fixed" is genuinely unpatchable today. A finding with a FixedVersion is a to-do list, not a dead end. Reading the severity first and stopping there is how you ship a fixable CVE for a year.
3. The wrong conclusion
Yesterday’s post ranked this image third, labelled it “acceptable, not clean”, and wrote that the single unrated finding was the price of admission. That framing was wrong in a way worth being precise about, because it is the most common way a security review goes stale.
I had reasoned: the base image is eclipse-temurin:21.0.12.1_1-jre-alpine, an upstream tag I do not control, so its package versions are whatever Eclipse chose on their build date. That is a fact about the tag, not about the CVE. It never followed that the tag could not be remediated on top of.
The test I should have run before writing a single word about severity was the cheapest one available: ask the package manager what it thinks is available.
4. The real cause
Alpine publishes to a rolling repository. The image was built when Alpine 3.24.2’s main had libpng 1.6.58-r1 as current. By the time of the scan, the live repository at dl-cdn.alpinelinux.org/alpine/v3.24/main carried libpng 1.6.59-r0 — the fixed version. Nothing had been missed. The image was simply frozen at build time while the distribution moved on.
A published tag is frozen at build time. The fix existed in the repository the whole time — the image simply never asked for it.
5. The fix
One line, placed before anything else touches the filesystem:
One added line. The `FROM` and the user creation are untouched.
Two details in that placement are load-bearing rather than stylistic. It runs before the user is created, so every subsequent operation links against the patched libraries rather than the ones being replaced. And it is a full upgrade rather than a targeted add libpng, which is why it quietly brought four other packages forward with it.
Why not apk upgrade libpng? It would also have worked here, and it is the right instinct when you want a minimal diff. It is the wrong instinct as a default: a package you did not think to name is a package that stays vulnerable after the next rebuild. Upgrading everything costs a few megabytes and removes the entire class of bug.
6. What it upgraded
Five packages moved. I named one; four came along.
| Package | Before | After | Why it mattered |
|---|---|---|---|
| libpng | 1.6.58-r1 | 1.6.59-r0 | The finding itself. Clears CVE-2026-46675. |
| openssl | 3.5.8-r0 | 3.5.9-r0 | Not flagged by this scan. Would have been flagged by the next one. |
| libssl3 | 3.5.8-r0 | 3.5.9-r0 | Same patch train as openssl. |
| libcrypto3 | 3.5.8-r0 | 3.5.9-r0 | Same patch train as openssl. |
| libldap | 2.6.14-r0 | 2.6.15-r0 | Carried into the image by the JRE’s base, not requested by anything. |
Only the first row was visible in the scan. The other four were latent — no CVE was published against them at scan time, but they were one disclosure away from being four new findings on the same image. Four of the five are OpenSSL components moving together on the same patch release, which is exactly the shape of a coordinated security release you do not want to be behind on.
7. Verification
A clean scan proves nothing if the image does not run, so the order was: rebuild, prove the libraries changed inside the image, boot it, then scan, then push.
| Check | Result |
|---|---|
| Patched versions present in the built image | libpng-1.6.59-r0, openssl-3.5.9-r0, libssl3-3.5.9-r0, libcrypto3-3.5.9-r0, libldap-2.6.15-r0 |
| Still runs as non-root | User=app, UID 65532 |
| Application starts | HTTP 200 on /petclinic/actuator/health after 10s |
| Health body | {“groups”:[“liveness”,“readiness”],“status”:“UP”} |
| Rescan | OS 0, application 0, total 0 |
| Pushed and read back | quay.io/hellodk/base-image:temurin-4.0.2-java21-cvefix3 |
| Digest confirmed from the registry | sha256:b924ecd3c5a8fc87ee436b813e132ce536bda46cb60028cbc9b60a3500d5fa0f |
The digest step matters more than it looks. A tag is a mutable pointer; a digest is the content. Reading the digest back out of the registry after the push is the only way to assert that what you scanned is what anyone else will pull — and it is the assertion most teams skip, which is how a scan result quietly stops describing the running image.
8. What it cost
| Metric | Before (cvefix2) | After (cvefix3) | Change |
|---|---|---|---|
| Findings | 1 | 0 | −1 |
| Registry transfer | 132.8 MB | 135.9 MB | +3.1 MB (+2.3%) |
| Local uncompressed | 280 MB | 288 MB | +8 MB |
| Compressed layers | 8 | 9 | +1 |
Three point one megabytes, one extra layer, one fewer finding. The fix also changed the image’s rank: it moved from third-and-not-clean to the smallest scan-clean Java 21 image in the whole comparison set, at 135.9 MB against Hummingbird’s 145.5 MB. It still ships a BusyBox shell, which is why it did not become the recommendation — but “cannot be made clean” is no longer a true statement about it.
The honest summary of the trade. 2.3% more transfer in exchange for a use-after-free that a remote party can reach if your application parses attacker-supplied PNG data. If it does not, you have bought cheap insurance against a future that includes a content-type confusion bug. Either way the price was 3.1 MB.
9. The app layer too
Worth recording because it was the other half of the gap, and it was not fixed by a Dockerfile line at all.
A base image can only fix the OS layer. The application layer belongs to the POM, and no Dockerfile can compensate for it.
CVE-2026-41710 was a MEDIUM denial of service via cache exhaustion in org.springframework.retry:spring-retry 1.0.3.RELEASE, fixed in 2.0.13. It arrived transitively through spring-data-jdbc 1.2.1.RELEASE, which is why it was invisible until something forced full dependency resolution. Pinning it in dependencyManagement took Hummingbird and Corretto from one finding to zero and produced the cvefix2 tags.
The uncomfortable part is that this image was never vulnerable because of anything in the base image. It was vulnerable because of a transitive dependency resolved by a build tool, and no amount of base image remediation would ever have touched it. Scans report layers; fixes live in different places depending on which layer is dirty.
10. Lessons
- Read
FixedVersionbeforeSeverity. Severity tells you how much to care.FixedVersiontells you whether you can do anything today. An emptyFixedVersionis the only real dead end. UNKNOWNmeans unrated, not unfixable. The finding that caused this entire post had no CVSS vector and was patched the same week it was disclosed. Treating unscored as unfixable is how a one-line fix becomes a permanent footnote.- A base image tag is a build-time snapshot. Nobody is going to rebuild someone else’s tag when a CVE lands. If your runtime has a package manager,
apk upgrade/apt-get upgrade/dnf upgradein your own Dockerfile is the only remediation you control. - Upgrade everything, not the thing the scanner named. The four packages I did not ask for were latent findings. A targeted pin leaves the class of bug in place for the next rebuild.
- Immutable tags drift silently. Nothing failed, nothing alerted, no scan result changed. The image was quietly a version behind its own distribution for as long as it existed. Re-scan on a schedule, not only at build time.
- Assert the digest, not the tag. A tag can point at something other than what you scanned.
sha256:b924ecd3…cannot. - Measure the fix in the currency of the decision. 3.1 MB is a number. “Zero findings in a 135.9 MB image” is a decision. Convert before you argue.
11. Reproduce
Check what your own image is carrying before you check what it is scanning as:
The simulation is the important one: it tells you what is available without changing anything, which is the check that would have prevented yesterday’s conclusion.
12. Sources
- CVE-2026-46675 — use-after-free in
png_read_end - Alpine v3.24/main repository index — where
libpng 1.6.59-r0was found - Eclipse Temurin —
21.0.12.1_1-jre-alpine,sha256:51ab5e3302e7141ce665ca3ea85e8b5cd648eafbc3c0c90dd79d6537684e4555 - Trivy 0.70.0 — all scans, image and language packages
- quay.io/hellodk/base-image —
temurin-4.0.2-java21-cvefix3
Enjoyed this post?
Get the next one in your inbox — only when I ship something worth reading.
Newsletter form not configured.
Or follow on Substack for the newsletter.
Comments via GitHub Discussions
Comments not configured. Set GISCUS env vars to enable.