github snapotter-hq/SnapOtter v2.2.0

9 hours ago

SnapOtter v2.2.0

Most of 2.2 is hardening. A non-admin holding a delegated users:manage role could take over an instance, and every tool endpoint turned out to be missing its permission check. Upgrade if you run more than one user.

The rest is the long tail of self-hosting: downloads that started and never finished, presets that broke the moment you added a second file, containers that crash-looped while Postgres was still coming up. Two new tools landed too. Docs and the website are now translated into 21 languages, and the interface finally clears WCAG AA contrast.

Important

If you granted users:manage to a non-admin through a custom role, that role could take over administrator accounts on 2.0.0 through 2.1.0. Details in Security.

Security

  • Privilege escalation through a delegated users:manage role (GHSA-9xgh-95qh-2x7h). A non-admin holding users:manage through a custom role could reset the password of, or delete, a higher-privileged account, a built-in administrator included, and take over the instance. Password reset and account deletion now verify the caller's authority over the target, so a delegated role cannot reach above its own level. Affected: 2.0.0 through 2.1.0. Reported by 李春来 (Chunlai Li). (#616)
  • Every tool endpoint is now gated. Tool access was enforced route by route, so all 45 hand-written routes had to remember the same call and none of them did. A role without tools:use could still run image-to-pdf, erase-object, upscale, sign-pdf and the rest. The check now lives in one preHandler keyed off the matched route, so it covers sub-paths and any route added later. (#646)
  • Rate limiting was bypassable on Docker installs. The image shipped TRUST_PROXY=true, so the client IP came from a request header and a forged X-Forwarded-For walked past the login limiter. The default is now a private-network trust list. (#649)
  • Settings writes are checked per setting, not once at the door, and config import is now transactional so a rejected key cannot leave half a config applied. (#618)
  • Library filenames cannot escape their storage root. A crafted stored name, which a malicious 1.x SQLite import copies verbatim, could read or delete files outside the files directory. Only reachable after an operator imports an attacker-supplied 1.x SQLite database, which is the one path that lets a stored name be chosen. Reported by @Alpastx. (GHSA-55w2-8cqf-w969, #600)
  • Job cancellation enforces ownership, so an authenticated user can no longer cancel another user's job by its id. Reported by @Alpastx. (GHSA-wqxf-gj2p-689x, #599)
  • A re-audit of the whole 2.0 tree caught what was still open: a captured SAML assertion could be replayed, an MFA challenge survived any number of wrong codes, the per-request API-key lookup was a full table scan, MAX_WORKSPACE_SIZE_GB was dead config, and the SVG sanitizer let through unquoted javascript: hrefs. (#620)
  • Nine disclosed CVEs patched: fast-uri, svgo, sharp and tar transitively (#619), plus Pillow 12.3.0 (#517). RAW decoding on arm64 now builds LibRaw 0.22.2 from source instead of linking an unpatched system copy. (#649)

New Features

  • Rounded Crop: rounded-square and iOS-style squircle masks, with a corner-radius control. Built for favicons and app icons. (#602)
  • Remove GIF Background: strip the background from an animated GIF, WebP or APNG frame by frame and get a transparent animation back. (#502)
  • Object Eraser lasso and High Quality mode: drag a freeform loop instead of painting every pixel, and optionally install a diffusion inpainting bundle for cleaner large-area fills. (#503, #566)
  • Save as new, or overwrite. Editing a library file used to silently supersede the original. You now choose per edit, and the default keeps it. (#564, #577)
  • Aspect-ratio presets in Resize. Pick 1:1, 4:3 or 16:9 and the two dimensions stay locked, without upscaling. (#530)
  • Two-tier OCR: a fast tier is baked into the image and runs offline with no download, and a more accurate runtime installs on demand. (#519)
  • Type anywhere to search. Start typing on the dashboard or the homepage and it lands in the search box. (#644)
  • Documentation and website in 21 languages: the docs site, the landing pages and the API reference are all translated now, joining the app interface. A new low-resource deployment guide covers Raspberry Pi and 2 GB machines. (#548)
  • Clearer tool names: 18 ambiguous names now self-qualify, so "Compress" became "Compress Image". Tool ids and routes are unchanged, so nothing bookmarked breaks. (#520)

Improvements

  • WCAG AA contrast across the interface: the palette was retuned so vivid orange stays on fills while text and labels use accessible ink tokens, and all 57 focus indicators now clear the 3:1 non-text bar. (#567, #574)
  • Upscale and background removal stop looking frozen: the progress bar advances during inference instead of parking at 30%, and both tools now warn that a CPU-only host is much slower. (#605, #608)
  • Timeout messages name the real cause and account for CPU-only hosts instead of blaming the upload. (#596)
  • SnapOtter waits for Postgres and Redis at startup instead of crash-looping, in Compose and the all-in-one image alike. A Proxmox LXC install was restarting 76 times before this. (#537, #595)
  • Downloads survive a reverse proxy: SnapOtter asks nginx and compatible proxies not to buffer file responses, which is the usual reason a self-hosted download starts and never finishes. (#604, #607)
  • Compress PDF lands near the target size, and says so honestly when a target is out of reach. (#522)
  • Mobile tool controls stay reachable now that the app shells size to the dynamic viewport. (#559)
  • Convert Audio exposes a sample-rate setting. (#561)
  • Admins can relax the minimum password length, down to 1 for a trusted LAN instance. (#543)
  • MFA is self-service, the policy lockout is closed, and OIDC or SAML logins get a real MFA challenge instead of a hard block. (#531, #536)
  • A bare Error: Error now names its cause. Sharp encode failures, AI sidecar exits, non-JSON document sidecar output, and background-removal failures all used to arrive with the reason scrubbed off. (#532, #534, #535, #538, #612)
  • The help dialog is translated. Its shortcut labels and getting-started text were hardcoded English while finished translations sat unused in all 21 locale files. (#647)

Bug Fixes

  • iPhone HEIC files were rejected as unreadable before they reached the decoder that handles them. (#631)
  • Conversion presets failed on a second file: jpg-to-pdf and its image-to-pdf siblings, plus pdf-to-png, pdf-to-jpg and pdf-to-tiff, all returned Tool not found once you uploaded two files. (#633, #643)
  • PDF conversion presets lost their download button. (#629)
  • An unlimited processing timeout was not honored, and stalled progress streams now recover instead of leaving the interface waiting. (#638)
  • Downloads hung instead of failing when a stored file turned out shorter than its recorded length. (#617)
  • PDF page tools failed on short and encrypted files: Remove Pages defaulted to a page range no document under six pages has, and password-protected PDFs failed cryptically inside the worker. (#594)
  • PDF to Text silently returned an empty file for scanned PDFs. It now points you at OCR and serves text as UTF-8. (#603)
  • PDF to Word dropped colored text blocks and split them across the page. (#500)
  • Object Eraser left ghost remnants and blurred small objects in high-resolution images. (#501)
  • Stabilize Video wrote unplayable output without faststart. (#593)
  • Image editor repairs: rotate, flip, resize, levels, curves, filters and layer lock. (#597)
  • Sign PDF showed a blank canvas instead of reporting why a PDF failed to load. (#545)
  • The file library recorded 0x0 dimensions and skipped previews for HEIC, RAW and PSD uploads. (#636, #637)
  • Installing more than one AI bundle left the shared virtualenv multi-versioned and quietly broke three tools. (#649)
  • Converting to JXL at quality 1 through 4 returned a 500, and a missing ffmpeg was reported to you as a corrupt upload. (#649)
  • A transient Postgres outage stranded finished jobs, leaving output on disk with no row pointing at it. A reconciler now adopts that work instead of dropping it. (#649)
  • A Redis endpoint that changed address wedged every consumer while health checks still answered 200. (#649)
  • Website and docs fixes: localized links no longer drop # fragments or lowercase zh-CN, the docs nav stays inside the viewport on tablets, and neither site calls the GitHub API from your browser any more. (#516, #562, #570, #560)

Upgrade Notes

Nothing to migrate, but two shipped defaults changed:

  • TRUST_PROXY now defaults to loopback,linklocal,uniquelocal instead of trusting every peer. Docker bridge and Compose networks sit inside that range, so most setups need no change. If your reverse proxy reaches SnapOtter from a public address, set TRUST_PROXY explicitly, or rate limiting and audit logs will attribute every request to the proxy.
  • MAX_AI_JOBS_PER_USER defaults to 5 in-flight single-file AI jobs per user. Batch and pipeline AI runs stay uncapped.

New optional knobs: DB_STARTUP_TIMEOUT_MS, SUBPROCESS_MEMORY_LIMIT_MB (off by default) and GIF_BG_MAX_FRAMES.

Acknowledgements

A good part of this release started as someone else's bug report.

Code and contributions:

  • @mvanhorn ❤️: Rewrote the remove-background timeout failure into a message that says what to do about it, instead of a bare timeout. (#518, #494)
  • @EuanTop ❤️: Restored the download action on PDF conversion preset pages by making the synchronous route return the standard tool-result contract. (#629, #623)
  • @harshjainnn ❤️: Diagnosed the download that starts and never finishes, and proposed the socket-reset direction the fix was built on. (#617, #590)

Security disclosures:

  • 李春来 (Chunlai Li) ❤️ (@laijunyue): Privately disclosed the privilege escalation through a delegated users:manage role, with a full source-to-sink analysis and a working exploit chain. (GHSA-9xgh-95qh-2x7h, #616)
  • @Alpastx ❤️ (Alpesh Bhagwatkar): Disclosed an authenticated IDOR on job cancellation, and a path traversal in library stored filenames reachable after a malicious 1.x SQLite import, with a working proof of concept for each. (#599, #600)

Bug reports:

  • @riz467 ❤️: Real iPhone HEIC files rejected at validation, with the root cause worked out in the report. (#622)
  • @coupej ❤️: PDF preset pages hanging forever, and jpg-to-pdf failing as soon as a second file was added. Two distinct bugs, correctly separated. (#623, #627)
  • @linuxuser1 ❤️: PROCESSING_TIMEOUT_S=0 documented as unlimited but capped at five minutes. (#630)
  • @bezibaerchen ❤️: Convert Audio hid the sample-rate setting its own description promised. (#558)
  • @hell-toupee ❤️: The accurate OCR bundle failing to install with a libpaddle error. (#505)
  • @Michael1260 ❤️ and @And-CSH ❤️: Confirmed that OCR install failure independently and established that manual extraction works while the in-app installer does not, which pinned the bug to the installer. (#505)
  • @TomErnst1972 ❤️: Enabling the MFA-required policy locking an admin out of their own instance. (#515)
  • @thokich ❤️: Object erasing producing blurry, unusable fills, which drove both the HD inpainting rewrite and the optional high-quality bundle. (#141)
  • @Hennie-git ❤️: A Proxmox LXC install restarting 76 times against a Postgres that was not yet accepting connections. (community-scripts/ProxmoxVE#15796)
  • MickLesk ❤️ (Proxmox VE community-scripts): Triaged that report in real time and produced the diagnosis the startup fix was built on. (#537)

Feature requests and feedback:

  • @LECOQQ ❤️: Asked to relax password complexity on a home-server install. (#136)
  • @killervette42 ❤️: Asked for an Unraid Community App, now published to the Unraid CA store. (#96)
  • @alienatedsec ❤️: Pointed out the GPU-falls-back-to-CPU fix was buried in a closed issue and undiscoverable, which is why it is in the deployment docs now. (#490, #587)
  • @neilp316 ❤️: Confirmed the Blackwell GPU failure on an RTX 5060 and mapped a working CUDA 12.8 upgrade path. (#120)
  • @Roiki11 ❤️: Argued for shared storage with path references over HTTP file transfer for off-box AI compute. (#189)

Thank you as well to the community members who reported these over Discord and email, whose names we did not record: the squircle crop request (#602), the image-to-PDF download that started and never finished (#604, #607), the Delete Pages tool getting stuck (#594), Arabic text missing from pdf-to-text (#603), and the diagnosis behind the ONNX GPU fallback (#490).

Upgrade

docker compose pull && docker compose up -d

Or pull the app image directly:

docker pull snapotter/snapotter:2.2.0

Full Changelog: v2.1.0...v2.2.0

Docs  ·  Live demo  ·  Website  ·  Discord

Don't miss a new SnapOtter release

NewReleases is sending notifications on new releases.