A security pass
A patch on top of 0.48.3: every open alert on the repository's Security tab, plus a manual review of the code. Upgrading is recommended, especially if you run a 3D-scan (splat) worker.
Fixed — scan storage trusted paths a worker could rewrite
The splat workers report their results by updating the scans row directly in Postgres, with their own credentials. The server then took two of that row's columns — where the scan's files live, and which file is the trained model — at face value, both when serving a scan and when deleting one.
So anything able to write that table could make the server stream, or delete, any object in the bucket: another user's invoices, their photos, the frames of someone else's scan. That takes database write access, which in practice means a compromised or hostile worker, so this is not reachable by an ordinary user of the app. Workers on hardware you don't fully control are the case to worry about.
A scan's files are now always located from the scan's own id, the same way they were laid out at creation; the columns are still written for the workers, but the server no longer believes them.
Two storage leaks close with it: deleting a scan from its page left a 3D scan's model and capture video in the bucket, and .m4v capture videos were never purged at all.
Fixed — dependencies
- All eleven Dependabot alerts in the web client: adm-zip, sharp, browserslist, baseline-browser-mapping, fast-uri, vitest. The adm-zip symlink advisory still lists "no fix"; 0.6.1 does contain it, under a different commit than the one the advisory cites.
- Two Rust vulnerabilities Dependabot never raised, found with
cargo audit: rustls (TLS 1.3 handshake messages accepted across encryption levels — it carries every outbound TLS connection the server makes) and crossbeam-epoch. Plus two crates flagged unsound and four yanked ones.
Every Rust bump was surgical, and verified with the locked release build rather than a host check.
Hardening
None of these was exploitable as found; each removes a way a future change could make it so.
- Pasted shop URLs. An orzgk wishlist URL was fetched exactly as pasted once its host checked out, so the port and scheme stayed the caller's to choose — enough to make the server probe ports on a third party. It's now rebuilt from its path, like product URLs already were.
- Outbound address filter. User-chosen destinations (webhooks, ntfy, Apprise, push endpoints, MangaCollector servers) now also refuse
0.0.0.0/8, and IPv4 addresses embedded in IPv6 through NAT64, which on a NAT64 network can reach the cloud metadata service. - Links from third-party data. A crafted tracking link could reach the page as a
javascript:URL. React 19 already neutralises those at render time, so this was never exploitable, but the app now enforces http(s) itself on every scraped or externally-sourced link rather than relying on that. - Headers. The Content-Security-Policy gains
object-src 'none', and the last proxied route now overwrites a client-suppliedX-Forwarded-Forlike all the others. - Supply chain. Every GitHub Action the build runs is pinned to a commit SHA, since a tag like
@v4can be moved by whoever controls the action, and the release workflow publishes these images.
The CodeQL alert (#9) was a false positive; its bound is now explicit in the code where the analyser can see it.
🔐 Security contract
🐳 ghcr.io/dim145/figurecollector-{server,client,docs}:0.48.4
📝 Full changelog: 0.48.3...0.48.4