The Python sidecar is a local HTTP service with access to files owned by the current user. Binding to loopback alone does not authenticate clients. Metadata and file contents must be treated as untrusted input, including when the user opens them through the desktop UI.
- Electron generates a random 256-bit session token and passes it through
METALENS_SIDECAR_TOKEN. Python removes it from its environment after reading it. - Every operation, including
/health, requires an Authorization bearer token. - Foreign browser origins are rejected. Allowed origins are the packaged file origin
(
null) and Vite at localhost or 127.0.0.1 on port 5173; these still require the token. - Electron validates the main frame and its exact application URL before answering IPC.
- The renderer uses context isolation, the sandbox, a Content Security Policy and a narrow preload bridge. Browser navigation, popups and unrelated permissions are denied.
This is not a filesystem sandbox: an authenticated user may intentionally select any file allowed by operating-system permissions. Normalization is not an authorization mechanism. Do not dismiss path-related findings solely because the service listens on loopback.
Every API mutation (/write, /delete, /undo) runs in ONE secure_atomic_write
transaction; the handler hooks (snapshot, apply_write/apply_delete, restore) work on
the temporary copy and never open a nested transaction. BaseMetadataHandler.write/delete
remain public wrappers that open their own transaction (_atomic_write).
- Resolve the target and verify it is an accessible regular file; refuse it with
TargetNotWritableError(HTTP 403) if it is not writable (permissions or the Windows READONLY attribute) or no temporary directory can be created beside it. - Serialize in-process writers and create an exclusively allocated private temporary
directory (
.metalens-*) on the same filesystem as the target. - Copy the file (same name as the target, inside the private directory). On Windows, clear HIDDEN/SYSTEM/READONLY on the copy while the callback runs (libraries recreate files by name, which Windows refuses for hidden files).
- Validate and apply all changes to the copy, then read it back: written keys must hold the
canonical value, deleted keys must be gone, no other field may change (else HTTP 422
lossy_write, original untouched). Preserve Unix ownership where required, flush, reject non-regular temporary files. - Check that the original file's identity and recorded stat attributes have not changed (else HTTP 409).
- Replace using
os.replaceon Unix orReplaceFileWon Windows. The Windows operation preserves the original security descriptor and alternate data streams; the original file attributes are put back on the copy before the replace and re-applied afterwards if needed. The target is a new file: hard links to the old one are detached. - Clean up only the operation's private workspace. Windows replacement uses a private backup for rollback; if rollback itself fails, retain recovery data and report its path.
Writer errors before replacement leave the original untouched. Concurrent-change checks reduce accidental overwrites; they do not guarantee protection against a malicious process with equivalent OS privileges or a hostile parent directory. Atomic visibility does not provide an unconditional guarantee of power-loss durability.
api/undo_journal.py keeps, in sidecar memory only, the raw pre-images and post-images of
the keys each save changed (max 50 entries / 64 MiB, FIFO eviction), addressed by a random
opaque token. /undo accepts only the token: the path comes from the journal, never from
the client. Undo is refused (409) when a touched key no longer holds its post-image or the
file was replaced (device/inode check); unknown, used or evicted tokens get 410. The journal
may contain metadata values (including binaries) of recently edited files and is lost when
the sidecar stops.
- JPEG metadata is rewritten by
core.jpeg_segments: only the touched APP1 Exif / APP1 XMP / APP13 Photoshop segment changes; every other segment, the scan data and trailers stay byte-identical. Duplicate Exif segments are removed when EXIF is rewritten, so deleted fields (e.g. GPS) do not survive in an older copy. The EXIF codec keeps every entry with its original type and bytes; the MakerNote is kept as opaque bytes at its original absolute offset (Canon-style pointers stay valid; vendor data stored outside the MakerNote itself is not tracked, hence a warning). Saving values equal to the current ones changes nothing: the file is not even replaced. - Other image formats (TIFF, RAW, PNG, WebP, GIF, BMP) are read-only.
- Encrypted PDFs are read-only to avoid accidentally removing encryption; digitally signed
PDFs (
/SigFlags, a signed/FT /Sigfield,/Perms) are read-only to avoid invalidating the signature. pypdf rewrites the whole PDF on save (content is preserved, the file is not byte-identical). - Office edits rewrite
docProps/core.xmlonly, preserving the other package parts and macros. Digitally signed Office packages are read-only. - XMP packets with DOCTYPE or entity declarations are refused.
- Audio: AAC and formats outside an explicit list of mutagen classes are read-only.
- The API derives capabilities from server-side handlers and ignores client flags; values are validated and canonicalized before the file is touched; library value errors become 422.
Regression tests cover rollback, colliding temporary names, metadata preservation, mixed
operations, authentication, NTFS streams/security descriptors, hidden and read-only files on
Windows, exact Undo, and a generic contract run on every synthetic sample in
tests/factories. CI runs the Python suite on both ubuntu-latest and windows-latest.
Real Unix ownership and symlink behavior must also be tested on a supported Unix system.
Malformed-file resource exhaustion cannot be ruled out by unit tests; keep parser dependencies
current and retain backups of important files.
References: FastAPI CORS, Python tempfile, Microsoft ReplaceFileW, Electron security.