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.
This commit is contained in:
Ethan Stoner
2026-08-27 15:26:01 -07:00
parent 2f960c6a03
commit 1a224919b2
3 changed files with 23 additions and 9 deletions
+22 -3
View File
@@ -19,9 +19,15 @@ from constants import USER_DATA_DIR, PROFILE_NAME
ENV_VAR = "REWARDS_ACCOUNTS" ENV_VAR = "REWARDS_ACCOUNTS"
# Names become directory names, so keep them to something a filesystem and a # 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._-]+$") SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
# Reserved by every filesystem that has directories at all.
RESERVED_NAMES = {".", ".."}
@dataclass(frozen=True) @dataclass(frozen=True)
class Account: class Account:
@@ -40,9 +46,22 @@ def _named(name: str) -> Account:
# Each account gets its own directory under the configured one, so the # 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 # existing data-dir stays where it is and the new ones sit beside the
# profile it already holds. # 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( return Account(
name=name, name=name,
user_data_dir=os.path.join(USER_DATA_DIR, name), user_data_dir=user_data_dir,
profile_name=PROFILE_NAME, profile_name=PROFILE_NAME,
) )
@@ -68,7 +87,7 @@ def configured() -> list[Account]:
accounts: list[Account] = [] accounts: list[Account] = []
for name in names: for name in names:
if not SAFE_NAME.match(name): if not SAFE_NAME.match(name) or name in RESERVED_NAMES:
raise ValueError( raise ValueError(
f"{ENV_VAR} entry {name!r} is not usable as a directory name; " 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"
-5
View File
@@ -5,8 +5,6 @@ import sys
import log_utils import log_utils
import accounts import accounts
import rewards_tasks import rewards_tasks
import mouse_trajectory
import mimic_typing
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException from selenium.common.exceptions import SessionNotCreatedException
@@ -54,9 +52,6 @@ def run_account(account: accounts.Account) -> bool:
return False return False
try: try:
mouse = mouse_trajectory.MouseUtils(driver)
keyboard = mimic_typing.KeyboardUtils(driver)
rewards = rewards_tasks.RewardsTaskUtils(driver) rewards = rewards_tasks.RewardsTaskUtils(driver)
rewards.complete_all_tasks() rewards.complete_all_tasks()
finally: finally:
+1 -1
View File
@@ -49,7 +49,7 @@ def search_query_for_task(task_description: str) -> str:
# Every feed was unreachable. The description still contains the topic, # Every feed was unreachable. The description still contains the topic,
# so a trimmed version beats skipping the card entirely. # 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() return task_description.lower()