From 0f18795f51b8924b42f05df908f7a1b752079224 Mon Sep 17 00:00:00 2001 From: Ethan Stoner Date: Fri, 28 Aug 2026 08:53:37 -0700 Subject: [PATCH] refuse profile names that Win32 resolves onto another account's directory Win32 strips a trailing dot off a path component and python's normalisation does not, so such a name is not the directory it reads as. REWARDS_ACCOUNTS=... resolved to data-dir itself, which is the default profile the resolved-path check was added to keep named accounts out of, and personal,personal. passed the duplicate check as two entries while sharing one profile on disk. Both break the one-directory-per-account guarantee this module exists for. Reject the shape by name, and resolve with realpath rather than abspath so a link or a junction under data-dir is followed to where it really goes. --- src/accounts.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/accounts.py b/src/accounts.py index dce2725..fe03a4c 100644 --- a/src/accounts.py +++ b/src/accounts.py @@ -50,9 +50,11 @@ def _named(name: str) -> Account: # The name passed the character check, but that only constrains the # characters, not where they end up pointing. Confirm against the resolved - # path, which is the thing Edge is actually handed. - root = os.path.abspath(USER_DATA_DIR) - resolved = os.path.abspath(user_data_dir) + # path, which is the thing Edge is actually handed. realpath rather than + # abspath, so a link or a junction under the profile directory is followed + # to where it really goes instead of being taken at face value. + root = os.path.realpath(USER_DATA_DIR) + resolved = os.path.realpath(user_data_dir) if os.path.commonpath([root, resolved]) != root or resolved == root: raise ValueError( @@ -87,10 +89,15 @@ def configured() -> list[Account]: accounts: list[Account] = [] for name in names: - if not SAFE_NAME.match(name) or name in RESERVED_NAMES: + # The trailing dot is not cosmetic. Win32 strips one off a path + # component and python's normalisation does not, so such a name means a + # different directory than it reads as: "work." is "work", and "..." is + # the profile directory itself. Either way two entries end up sharing + # one profile, which is the one thing this module exists to prevent. + if not SAFE_NAME.match(name) or name in RESERVED_NAMES or name.endswith("."): raise ValueError( f"{ENV_VAR} entry {name!r} is not usable as a directory name; " - "use letters, digits, dot, dash or underscore" + "use letters, digits, dot, dash or underscore, and do not end in a dot" ) # Duplicates would run the same profile twice, which earns nothing the