Skip to content

fix: accurate prune sizes, shared-image and revert tracking, and security hardening - #103

Merged
StrandedTurtle merged 1 commit into
mainfrom
ccr-44bed3b3-tyenjv
Oct 5, 2026
Merged

StrandedTurtle merged 1 commit into
mainfrom
ccr-44bed3b3-tyenjv

Conversation

@StrandedTurtle

Copy link
Copy Markdown
Owner

Summary

Prune was over-reporting (the "15 GB")

Docker's image Size is the whole image, including base layers still used by your current images. The preview and the "Freed X" message summed those, so they were wildly inflated. In a reproduction on real Docker, three leftovers showed 1.26 GB when they actually freed 60 MB.

  • The preview now shows what each image really frees: its size minus layers shared with other images. Docker only computes SharedSize among images in the same response, so it's taken from the unfiltered list.
  • "Freed X" is now measured (image-layer disk usage before minus after), not estimated.
  • Untagged images still used by a container are no longer offered. Docker would refuse to delete them anyway.
  • "Prune everything" now counts images rather than Docker's untag/layer entries.
  • Revert points whose image was pruned are dropped. The dialog warns before pruning one, and Revert now refuses cleanly (410) if its image is gone.
  • The Settings "needs pruning" badge now refreshes after a prune instead of waiting for the daily scan.

Bugs found by running the app against a real Docker daemon

  • A container whose tag moved (e.g. a sibling on the same image updated first) or that was reverted reported "up to date" forever. A reverted standalone container also couldn't be updated, because DockPull tried docker pull sha256:….
  • Revert copied Compose's com.docker.compose.image label, so a later compose update silently did nothing. That label is fixed now, and there's a force-recreate safety net after compose up.
  • No revert point and blank history versions were recorded when the old image's digest was unknown. Rollback is now tracked by image ID, and versions are stored on history rows.
  • Loopback registries (localhost:5000) are now queried over http, as Docker does.

Security

  • CSRF: state-changing API calls require an X-DockPull: 1 header. SameSite=Lax doesn't protect against other apps on the same host on a different port.
  • Container names are validated before reaching the Docker API, which puts them into request paths unescaped.
  • The session cookie is bound to ADMIN_PASSWORD, so changing the password signs everyone out. Existing sessions need to log in once after upgrading.
  • Registry credentials are only sent to https token servers.

Testing

  • 149 server tests pass, including a prune test against a fake Docker API socket.
  • Full lifecycle run live on Docker 29 (containerd store) with a local registry:
    • check, and compose + standalone update
    • revert, and re-update after revert
    • shared-image updates
    • prune preview, partial prune and prune-all, with freed space verified against docker system df
    • missing-image revert
    • skip/pin, settings, history, logout, CSRF and name validation
  • The real UI was driven with Playwright through login, check, and the prune dialog, with no console errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KiL5R3bRpvGk6QP3E21eHF


Generated by Claude Code

…rity hardening

Prune reported each leftover image's WHOLE size (Docker's `Size` includes
base layers still used by current images), so the preview and the "Freed X"
message were wildly inflated. Now:
- preview shows what each image really frees (Size minus layers shared with
  other images; SharedSize taken from the unfiltered list, since Docker only
  computes it among the images in the same response)
- "Freed X" is measured (image-layer disk usage before/after)
- untagged images still used by a container are no longer offered
- prune-all counts images, not untag/layer entries
- revert points whose image was pruned are dropped (and Revert refuses
  cleanly with 410 if its image is gone), and the Settings badge refreshes

Found by running the app against a real Docker daemon:
- containers on a tag that moved (sibling updated first) or that were
  reverted reported "up to date" forever; reverted standalone containers
  couldn't be updated (pull of a bare image ID)
- revert copied Compose's image label, so a later compose update was a
  silent no-op; also added a force-recreate safety net
- no revert point / blank history versions when the old image's digest was
  unknown (tracked by image ID now; versions stored on history rows)
- loopback registries (localhost:5000) are spoken to over http, like Docker

Security:
- CSRF: state-changing API calls require an X-DockPull header (SameSite=Lax
  doesn't cover other apps on the same host)
- container names validated before reaching the Docker API
- session cookie bound to ADMIN_PASSWORD (changing it signs everyone out)
- registry credentials only sent to https token servers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiL5R3bRpvGk6QP3E21eHF
@StrandedTurtle
StrandedTurtle merged commit 04683e4 into main Oct 5, 2026
2 checks passed
@StrandedTurtle
StrandedTurtle deleted the ccr-44bed3b3-tyenjv branch October 5, 2026 08:38
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.

2 participants