From 1a224919b23515a6bf141d2715cd0f410c2bd7c8 Mon Sep 17 00:00:00 2001 From: Ethan Stoner Date: Thu, 27 Aug 2026 15:26:01 -0700 Subject: [PATCH] reject profile names that resolve outside data-dir The character check allowed "." and "..", which are made entirely of allowed characters and still walk out of the directory, so the traversal guard only stopped the cases containing a separator. Reject both by name and check the resolved path against the profile root as well, since the character set constrains the characters rather than where they point. Also drop the MouseUtils and KeyboardUtils built in run_account and never used, RewardsTaskUtils builds its own, along with the two imports that leaves unused, and say "lowercased" in the no-source warning, which is what the code returns. --- src/accounts.py | 25 ++++++++++++++++++++++--- src/main.py | 5 ----- src/queries.py | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/accounts.py b/src/accounts.py index 642f773..dce2725 100644 --- a/src/accounts.py +++ b/src/accounts.py @@ -19,9 +19,15 @@ from constants import USER_DATA_DIR, PROFILE_NAME ENV_VAR = "REWARDS_ACCOUNTS" # Names become directory names, so keep them to something a filesystem and a -# command line both handle without quoting. +# command line both handle without quoting. The character set alone is not +# enough: "." and ".." are made of allowed characters and still walk out of the +# directory, so they are rejected by name below and the resolved path is +# checked as well. SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$") +# Reserved by every filesystem that has directories at all. +RESERVED_NAMES = {".", ".."} + @dataclass(frozen=True) class Account: @@ -40,9 +46,22 @@ def _named(name: str) -> Account: # Each account gets its own directory under the configured one, so the # existing data-dir stays where it is and the new ones sit beside the # profile it already holds. + user_data_dir = os.path.join(USER_DATA_DIR, name) + + # 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) + + if os.path.commonpath([root, resolved]) != root or resolved == root: + raise ValueError( + f"{ENV_VAR} entry {name!r} resolves outside the profile directory" + ) + return Account( name=name, - user_data_dir=os.path.join(USER_DATA_DIR, name), + user_data_dir=user_data_dir, profile_name=PROFILE_NAME, ) @@ -68,7 +87,7 @@ def configured() -> list[Account]: accounts: list[Account] = [] for name in names: - if not SAFE_NAME.match(name): + if not SAFE_NAME.match(name) or name in RESERVED_NAMES: raise ValueError( f"{ENV_VAR} entry {name!r} is not usable as a directory name; " "use letters, digits, dot, dash or underscore" diff --git a/src/main.py b/src/main.py index 074a99c..8ce6b7a 100644 --- a/src/main.py +++ b/src/main.py @@ -5,8 +5,6 @@ import sys import log_utils import accounts import rewards_tasks -import mouse_trajectory -import mimic_typing from selenium import webdriver from selenium.common.exceptions import SessionNotCreatedException @@ -54,9 +52,6 @@ def run_account(account: accounts.Account) -> bool: return False try: - mouse = mouse_trajectory.MouseUtils(driver) - keyboard = mimic_typing.KeyboardUtils(driver) - rewards = rewards_tasks.RewardsTaskUtils(driver) rewards.complete_all_tasks() finally: diff --git a/src/queries.py b/src/queries.py index 6f166e7..cca3efd 100644 --- a/src/queries.py +++ b/src/queries.py @@ -49,7 +49,7 @@ def search_query_for_task(task_description: str) -> str: # Every feed was unreachable. The description still contains the topic, # so a trimmed version beats skipping the card entirely. - logger.warning("No query source reachable, using the task description as written.") + logger.warning("No query source reachable, using the lowercased task description.") return task_description.lower()