Photo Curator is a 100% local desktop tool. It runs a small Flask server bound
to 127.0.0.1 and opens a browser tab against it. No data leaves your machine:
no uploads, no telemetry, no external APIs (the only outbound requests are
optional OpenFreeMap vector map tiles in the GPS view and a Ko-fi donate badge,
both loaded by your browser, not the app; the map library itself is bundled, not
loaded from a CDN).
This document describes the threat model and the mitigations in place.
The app reads, copies, moves, and serves image files from a folder you select. The realistic threats for a localhost tool are therefore:
- A malicious web page reaching the local server. Any site you visit can
make your browser send requests to
http://127.0.0.1:5014. Via a DNS-rebinding attack a remote site can even make those requests appear same-origin. If the server served arbitrary files, such a page could read your photos — or any file — off disk. - Path traversal / arbitrary file read. A crafted
?path=parameter (../../etc/passwd, an absolute path, or a symlink) could escape the selected folder. - Network exposure. If the server bound to
0.0.0.0it would be reachable by other devices on your LAN. - Supply-chain / dependency vulnerabilities. A compromised or outdated third-party package.
- Accidental data loss. A bug or mistaken click destroying originals.
Out of scope: an attacker who already has local code-execution or filesystem access on your machine (they don't need this app), and multi-user/hostile-LAN server hardening (the app is single-user and loopback-only).
| Threat | Mitigation |
|---|---|
| Malicious page / DNS rebinding (1) | Every request is rejected unless its Host header is a loopback address (127.0.0.1/localhost), and any cross-site Origin is rejected (HTTP 403). A rebound attacker domain fails the Host check. |
| Path traversal / arbitrary read (2) | The image, thumbnail, and EXIF endpoints resolve the requested path with os.path.realpath() (collapsing .. and following symlinks) and serve it only if it is a real image file inside the currently selected or a recently used folder. Everything else returns 404. |
| Network exposure (3) | The server binds explicitly to 127.0.0.1, never 0.0.0.0. It is unreachable from the network. |
| Dependency vulnerabilities (4) | Dependencies are pinned to exact versions in requirements.txt. Scan them with pip-audit -r requirements.txt. |
| Accidental data loss (5) | Actions are reversible and explicit: nothing is deleted. Blurry photos move to a Blurred/ subfolder, exports copy originals into TOP_N/ and PhoneBG/. Originals are never modified in place. |
| Response hardening | Security headers on every response: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, and a Content-Security-Policy. |
Each release ships a SHA-256 checksum (see CHECKSUMS.txt). Verify before
running:
# macOS
shasum -a 256 -c CHECKSUMS.txt
# Linux
sha256sum -c CHECKSUMS.txtIf the checksum does not match, do not run the file — re-download from the official source.
This is an MIT-licensed hobby project. Please open an issue (or, for anything sensitive, contact the maintainer privately) describing the problem and how to reproduce it. Since the app is local-only and stores no credentials, the blast radius of most issues is limited to the machine running it.