From e6755aa8a1fe85daf81ac5ba462f96c26ff505ba Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 3 Sep 2025 21:55:07 +0000 Subject: [PATCH] restrict runtime-state in $TMP; closes #747 the preferred locations (XDG_CONFIG_HOME and ~/.config) are trusted and will behave as before, because they are only writable by the current unix-user but when an emergency fallback location ($TMPDIR or /tmp) is used because none of the preferred locations are writable, then this will now force-disable sessions-db, idp-db, chpw, and shares this security safeguard can be overridden with --unsafe-state will now also create the config folder with chmod 700 (rwx------) --- README.md | 1 + copyparty/__init__.py | 1 + copyparty/__main__.py | 31 +++++++++++++++++++++---------- copyparty/svchub.py | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index df138d2f..f091d122 100644 --- a/README.md +++ b/README.md @@ -2318,6 +2318,7 @@ buggy feature? rip it out by setting any of the following environment variables | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) | | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy | | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider | +| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations | example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py` diff --git a/copyparty/__init__.py b/copyparty/__init__.py index d1630ba0..a21e520e 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -112,6 +112,7 @@ class EnvParams(object): self.t0 = time.time() self.mod = "" self.cfg = "" + self.scfg = True E = EnvParams() diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 5c6a8979..fedef803 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -36,6 +36,7 @@ from .__init__ import ( ) from .__version__ import CODENAME, S_BUILD_DT, S_VERSION from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt +from .bos import bos from .cfg import flagcats, onedash from .svchub import SvcHub from .util import ( @@ -186,7 +187,7 @@ def init_E(EE: EnvParams) -> None: E = EE # pylint: disable=redefined-outer-name - def get_unixdir() -> str: + def get_unixdir() -> tuple[str, bool]: paths: list[tuple[Callable[..., Any], str]] = [ (os.environ.get, "XDG_CONFIG_HOME"), (os.path.expanduser, "~/.config"), @@ -197,6 +198,8 @@ def init_E(EE: EnvParams) -> None: ] errs = [] for npath, (pf, pa) in enumerate(paths): + priv = npath < 2 # private/trusted location + ram = npath > 1 # "nonvolatile"; not semantically same as `not priv` p = "" try: p = pf(pa) @@ -206,15 +209,21 @@ def init_E(EE: EnvParams) -> None: p = os.path.normpath(p) mkdir = not os.path.isdir(p) if mkdir: - os.mkdir(p) + os.mkdir(p, 0o700) p = os.path.join(p, "copyparty") + if not priv and os.path.isdir(p): + uid = os.geteuid() + if os.stat(p).st_uid != uid: + p += ".%s" % (uid,) + if os.path.isdir(p) and os.stat(p).st_uid != uid: + raise Exception("filesystem has broken unix permissions") try: os.listdir(p) except: - os.mkdir(p) + os.mkdir(p, 0o700) - if npath > 1: + if ram: t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" errs.append(t % (pa, p)) elif mkdir: @@ -226,13 +235,14 @@ def init_E(EE: EnvParams) -> None: if errs: warn(". ".join(errs)) - return p # type: ignore + return p, priv except Exception as ex: - if p and npath < 2: + if p: t = "Unable to store config in %s [%s] due to %r" errs.append(t % (pa, p, ex)) - raise Exception("could not find a writable path for config") + t = "could not find a writable path for runtime state:\n> %s" + raise Exception(t % ("\n> ".join(errs))) E.mod = os.path.dirname(os.path.realpath(__file__)) if E.mod.endswith("__init__"): @@ -247,7 +257,7 @@ def init_E(EE: EnvParams) -> None: p = os.path.abspath(os.path.realpath(p)) p = os.path.join(p, "copyparty") if not os.path.isdir(p): - os.mkdir(p) + os.mkdir(p, 0o700) os.listdir(p) except: p = "" @@ -260,11 +270,11 @@ def init_E(EE: EnvParams) -> None: elif sys.platform == "darwin": E.cfg = os.path.expanduser("~/Library/Preferences/copyparty") else: - E.cfg = get_unixdir() + E.cfg, E.scfg = get_unixdir() E.cfg = E.cfg.replace("\\", "/") try: - os.makedirs(E.cfg) + bos.makedirs(E.cfg, bos.MKD_700) except: if not os.path.isdir(E.cfg): raise @@ -1453,6 +1463,7 @@ def add_yolo(ap): ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") + ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets") def add_optouts(ap): diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 50a3e2d4..eca7f8e0 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -976,6 +976,24 @@ class SvcHub(object): t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n" self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1) + if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"): + t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1." + if not al.no_ses: + al.no_ses = True + t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!" + self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1) + if al.idp_store == 1: + al.idp_store = 0 + self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3) + if al.idp_store: + t2 = "ERROR: Cannot enable --idp-store %s" % (t,) + self.log("root", t2, 1) + raise Exception(t2) + if al.shr: + t2 = "ERROR: Cannot enable shares %s" % (t,) + self.log("root", t2, 1) + raise Exception(t2) + def _process_config(self) -> bool: al = self.args