From c6965f061456305d1d7dbadf626931b0e45e082f Mon Sep 17 00:00:00 2001
From: exci <76759714+icxes@users.noreply.github.com>
Date: Sat, 7 Mar 2026 23:54:49 +0200
Subject: [PATCH] add optional update-checker (#1315)
can check if the current version has a known vulnerability, with the option to panic and exit if so, and otherwise show a warning in the controlpanel for admins
---------
Co-authored-by: ed
---
contrib/systemd/copyparty.example.conf | 9 +++
copyparty/__main__.py | 3 +
copyparty/broker_mp.py | 4 ++
copyparty/broker_thr.py | 17 +++--
copyparty/httpcli.py | 9 ++-
copyparty/httpsrv.py | 4 ++
copyparty/svchub.py | 86 ++++++++++++++++++++++++++
docs/chungus.conf | 9 +++
8 files changed, 134 insertions(+), 7 deletions(-)
diff --git a/contrib/systemd/copyparty.example.conf b/contrib/systemd/copyparty.example.conf
index 79560d0d..a28802b2 100644
--- a/contrib/systemd/copyparty.example.conf
+++ b/contrib/systemd/copyparty.example.conf
@@ -18,6 +18,15 @@
# (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
+ # enable version-checker by uncommenting one of the 'vc-url' lines below; this will
+ # periodically check if your copyparty version has a known security vulnerability,
+ # showing a warning on /?h (control-panel) for all users with permission 'a' or 'A'
+ #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9
+ #vc-url: https://api.copyparty.eu/advisories
+
+ vc-age: 3 # how many hours to wait between each version-check
+ vc-exit # emergency-exit if a version-check indicates that the current version is vulnerable
+
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
# ftp: 3921 # enable ftp server on port 3921
diff --git a/copyparty/__main__.py b/copyparty/__main__.py
index eda7e104..0f78f99f 100644
--- a/copyparty/__main__.py
+++ b/copyparty/__main__.py
@@ -1201,6 +1201,9 @@ def add_general(ap, nc, srvname):
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best")
+ ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)")
+ ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks")
+ ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable")
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
ap2.add_argument("--version", action="store_true", help="show versions and exit")
ap2.add_argument("--versionb", action="store_true", help="show version and exit")
diff --git a/copyparty/broker_mp.py b/copyparty/broker_mp.py
index 52c9afed..b1b2f9d9 100644
--- a/copyparty/broker_mp.py
+++ b/copyparty/broker_mp.py
@@ -157,6 +157,10 @@ class BrokerMp(object):
elif dest == "cb_httpsrv_up":
self.hub.cb_httpsrv_up()
+ elif dest == "httpsrv.set_bad_ver":
+ for p in self.procs:
+ p.q_pend.put((0, dest, list(args)))
+
else:
raise Exception("what is " + str(dest))
diff --git a/copyparty/broker_thr.py b/copyparty/broker_thr.py
index 43bea239..ae290d06 100644
--- a/copyparty/broker_thr.py
+++ b/copyparty/broker_thr.py
@@ -53,13 +53,18 @@ class BrokerThr(BrokerCli):
return NotExQueue(obj(*args)) # type: ignore
def say(self, dest: str, *args: Any) -> None:
- if dest == "httpsrv.listen":
- self.httpsrv.listen(args[0], 1)
- return
+ if dest.startswith("httpsrv."):
+ if dest == "httpsrv.listen":
+ self.httpsrv.listen(args[0], 1)
+ return
- if dest == "httpsrv.set_netdevs":
- self.httpsrv.set_netdevs(args[0])
- return
+ if dest == "httpsrv.set_netdevs":
+ self.httpsrv.set_netdevs(args[0])
+ return
+
+ if dest == "httpsrv.set_bad_ver":
+ self.httpsrv.set_bad_ver()
+ return
# new ipc invoking managed service in hub
obj = self.hub
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index 10c72381..49455062 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -156,6 +156,7 @@ BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff
BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n"
BADXFP = ', or change the copyparty global-option "xf-proto" to another header-name to read this value from. Alternatively, if your reverseproxy is not able to provide a header similar to "X-Forwarded-Proto", then you must tell copyparty which protocol to assume; either "--xf-proto-fb=http" or "--xf-proto-fb=https"'
BADXFFB = "NOTE: serverlog has a message regarding your reverse-proxy config"
+BADVER = 'Please upgrade copyparty; Your version has a vulnerability
(only users with permission "a" or "A" can see this message)
' H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -5625,7 +5626,13 @@ class HttpCli(object): no304=self.no304(), k304vis=self.args.k304 > 0, no304vis=self.args.no304 > 0, - msg=BADXFFB if hasattr(self, "bad_xff") else "", + msg=( + BADVER + if self.conn.hsrv.bad_ver and self.can_admin + else BADXFFB + if hasattr(self, "bad_xff") + else "" + ), ver=S_VERSION if show_ver else "", chpw=self.args.chpw and self.uname != "*", ahttps="" if self.is_https else "https://" + self.host + self.req, diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index ea89a0aa..a830f5c5 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -143,6 +143,7 @@ class HttpSrv(object): self.name = "hsrv" + nsuf self.mutex = threading.Lock() self.u2mutex = threading.Lock() + self.bad_ver = False self.stopping = False self.tp_nthr = 0 # actual @@ -239,6 +240,9 @@ class HttpSrv(object): except: pass + def set_bad_ver(self) -> None: + self.bad_ver = True + def set_netdevs(self, netdevs: dict[str, Netdev]) -> None: ips = set() for ip, _ in self.bound: diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 929392f7..7b69afcd 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals import argparse import atexit import errno +import json import logging import os import re @@ -27,6 +28,7 @@ if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode +from .__version__ import S_VERSION, VERSION from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who from .bos import bos from .cert import ensure_cert @@ -75,6 +77,7 @@ from .util import ( mp, odfusion, pybin, + read_utf8, start_log_thrs, start_stackmon, termsize, @@ -95,6 +98,11 @@ if TYPE_CHECKING: if PY2: range = xrange # type: ignore +if PY2: + from urllib2 import Request, urlopen +else: + from urllib.request import Request, urlopen + VER_IDP_DB = 1 VER_SESSION_DB = 1 @@ -1382,6 +1390,8 @@ class SvcHub(object): Daemon(self.tcpsrv.netmon, "netmon") Daemon(self.thr_httpsrv_up, "sig-hsrv-up2") + if self.args.vc_url: + Daemon(self.check_ver, "ver-chk") sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: @@ -1778,3 +1788,79 @@ class SvcHub(object): zb = gzip.compress(zb) zs = ub64enc(zb).decode("ascii") self.log("stacks", zs) + + def check_ver(self) -> None: + next_chk = 0 + # self.args.vc_age = 2 / 60 + fpath = os.path.join(self.E.cfg, "vuln_advisory.json") + while not self.stopping: + now = time.time() + if now < next_chk: + time.sleep(min(999, next_chk - now)) + continue + + age = 0 + jtxt = "" + src = "[cache] " + try: + mtime = os.path.getmtime(fpath) + age = time.time() - mtime + if age < self.args.vc_age * 3600 - 69: + zs, jtxt = read_utf8(None, fpath, True).split("\n", 1) + if zs != self.args.vc_url: + jtxt = "" + except Exception as e: + t = "will download advisory because cache-file %r could not be read: %s" + self.log("ver-chk", t % (fpath, e), 6) + + if not jtxt: + src = "" + age = 0 + try: + req = Request(self.args.vc_url) + with urlopen(req, timeout=30) as f: + jtxt = f.read().decode("utf-8") + try: + with open(fpath, "wb") as f: + zs = self.args.vc_url + "\n" + jtxt + f.write(zs.encode("utf-8")) + except Exception as e: + t = "failed to write advisory to cache; %s" + self.log("ver-chk", t % (e,), 3) + except Exception as e: + t = "failed to fetch vulnerability advisory; %s" + self.log("ver-chk", t % (e,), 1) + + next_chk = time.time() + 699 + if not jtxt: + continue + + try: + advisories = json.loads(jtxt) + for adv in advisories: + if adv.get("state") == "closed": + continue + vuln = {} + for x in adv["vulnerabilities"]: + if x["package"]["name"].lower() == "copyparty": + vuln = x + break + if not vuln: + continue + sver = vuln["patched_versions"].strip(".v") + tver = tuple([int(x) for x in sver.split(".")]) + if VERSION < tver: + zs = json.dumps(adv, indent=2) + t = "your version (%s) has a vulnerability! please upgrade:\n%s" + self.log("ver-chk", t % (S_VERSION, zs), 1) + self.broker.say("httpsrv.set_bad_ver") + if self.args.vc_exit: + self.shutdown() + return + else: + t = "%sok; v%s and newer is safe" + self.log("ver-chk", t % (src, sver), 2) + next_chk = time.time() + self.args.vc_age * 3600 - age + except Exception as e: + t = "failed to process vulnerability advisory; %s" + self.log("ver-chk", t % (min_ex()), 1) diff --git a/docs/chungus.conf b/docs/chungus.conf index aea1fabf..e2742ca0 100644 --- a/docs/chungus.conf +++ b/docs/chungus.conf @@ -57,6 +57,15 @@ # show versions and exit version + + # url to check against for vulnerable versions (default disabled); setting this will enable automatic + # vulnerability checks. the notification, in case you are running a vulnerable version, is shown on the + # admin panel (/?h) and only for users with admin permissions. you can choose between the value given here, + # or alternatively use https://api.copyparty.eu/security-advisories, or your own custom endpoint + vc-url: https://api.github.com/repos/9001/copyparty/security-advisories + + # how many seconds to wait between vulnerability checks; default is 86400 (= 1 day). + vc-interval: 86400 ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ ###// qr options \\000000000000000000000000000000000000000000000000000000000000000000000000000\