Phase 1 of the fix for #8228, simplified after KISS review.
The earlier draft (commit 5cb83b41a, since reverted) introduced a
multi-grant capability store with opaque grantIds and a feature
discriminator. Review showed the abstraction was overhead: this app
has exactly one user-pickable filesystem location (the sync folder).
Backgrounds will go through copy-to-cache, not folder grants;
plugins do not get folder grants; backups use a main-derived path.
And the opaque grantId added zero marginal security over a featureless
{relativePath} API since an XSS in the renderer would just replay the
id from memory. The security boundary is "main owns the canonical root
and the resolver logic", not the id.
So: drop the store, keep the resolver. resolveSyncPath takes
(syncFolderPath, relativePath, userDataDir) and returns the vetted
absolute path. Layered defenses:
- syncFolderPath canonicalized every call (folder may have moved
since startup); missing/inaccessible root denies
- canonical root must not be userData or live inside it (a user who
picks userData as their sync folder would otherwise hand the
renderer authority over settings/grants/db)
- relativePath may be '' or '.' for LIST/CHECK ops on the root;
anything else must not be absolute and must not escape
- leaf symlinks refused (v1 policy; O_NOFOLLOW alternative is a
follow-up that needs cross-platform care)
- canonical resolved path (or deepest existing ancestor when the
leaf doesn't exist yet) must still live inside the canonical
root — catches symlinks/junctions in intermediate directories
and case-fold / 8.3-shortname aliases
- ancestor walk fails CLOSED on EACCES; only ENOENT lets us walk
up. A permission-restricted ancestor must not silently allow.
Errors are opaque (PathNotAllowedError, no path in message) and the
stack is stripped — IPC handlers further sanitize before returning to
the renderer.
Phase 2 will wire this into FILE_SYNC_SAVE/LOAD/REMOVE/LIST_FILES and
CHECK_DIR_EXISTS, move syncFolderPath ownership from the renderer's
privateCfg to a main-side simple-store key, and add a re-confirm
migration on first launch.
16 unit tests cover the rejection cases from #8228's test plan plus
the EACCES-fail-closed branches that came out of the KISS review.