diff --git a/contrib/systemd/copyparty.example.conf b/contrib/systemd/copyparty.example.conf index 79560d0d..23558a86 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 + # 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 + # 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..8b189618 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1204,6 +1204,8 @@ def add_general(ap, nc, srvname): 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") + ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default: empty/disabled)") + ap2.add_argument("--vc-interval", metavar="SEC", type=int, default=86400, help="how many seconds to wait between vulnerability checks (default: 86400)") def add_qr(ap, tty): 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..b433a300 100644 --- a/copyparty/broker_thr.py +++ b/copyparty/broker_thr.py @@ -61,6 +61,10 @@ class BrokerThr(BrokerCli): self.httpsrv.set_netdevs(args[0]) return + if dest == "httpsrv.set_bad_ver": + self.httpsrv.set_bad_ver(args[0]) + return + # new ipc invoking managed service in hub obj = self.hub for node in dest.split("."): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 472c270c..de783ab7 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -155,7 +155,8 @@ ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" 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" +BADXFFB = "
NOTE: serverlog has a message regarding your reverse-proxy config
" +BADVER = "
The version of copyparty currently active has a known vulnerability (more info) that has been fixed; please update to the latest version. This message is only visible to users with the admin (a or A) permission.
" H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" @@ -5624,7 +5625,8 @@ 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=(BADXFFB if not hasattr(self, "bad_xff") else "") + + (BADVER if self.conn.hsrv.bad_ver and self.can_admin 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..728a3743 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -119,6 +119,7 @@ class HttpSrv(object): socket.setdefaulttimeout(120) self.t0 = time.time() + self.bad_ver = False nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else "" self.magician = Magician() self.nm = NetMap([], []) @@ -232,6 +233,9 @@ class HttpSrv(object): self.th_cfg: dict[str, set[str]] = {} Daemon(self.post_init, "hsrv-init2") + def set_bad_ver(self, val: bool) -> None: + self.bad_ver = val + def post_init(self) -> None: try: x = self.broker.ask("thumbsrv.getcfg") diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 929392f7..18fa5c40 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 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,12 +98,16 @@ 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 VER_SHARES_DB = 2 - class SvcHub(object): """ Hosts all services which cannot be parallelized due to reliance on monolithic resources. @@ -1382,6 +1389,7 @@ class SvcHub(object): Daemon(self.tcpsrv.netmon, "netmon") Daemon(self.thr_httpsrv_up, "sig-hsrv-up2") + Daemon(self.check_ver, "ver-chk") sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: @@ -1778,3 +1786,76 @@ class SvcHub(object): zb = gzip.compress(zb) zs = ub64enc(zb).decode("ascii") self.log("stacks", zs) + + def parse_version(self, ver: str) -> tuple: + if not ver or not isinstance(ver, str): + return (0, 0, 0) + match = re.search(r'[\d.]+', ver) + if not match: + return (0, 0, 0) + parts = [int(x) for x in match.group(0).split(".")] + while len(parts) < 3: + parts.append(0) + return tuple(parts[:3]) + + def get_vuln_cache_path(self) -> str: + return os.path.join(self.E.cfg, "vuln_advisory.json") + + def check_ver(self) -> None: + if not self.args.vc_url: + return + + ver_cpp = self.parse_version(S_VERSION) + + while not self.stopping: + fpath = self.get_vuln_cache_path() + data = None + + try: + mtime = os.path.getmtime(fpath) + if time.time() - mtime < self.args.vc_interval: + data = read_utf8(None, fpath, True) + except Exception as e: + self.log("ver-chk", "no cached vulnerability advisory found, fetching; {}".format(e)) + + if not data: + try: + req = Request(self.args.vc_url) + with urlopen(req, timeout=30) as f: + data = f.read().decode("utf-8") + + with open(fpath, "wb") as f: + f.write(data.encode("utf-8")) + + except Exception as e: + self.log("ver-chk", "failed to fetch vulnerability advisory; {}".format(e)) + + if data: + try: + advisories = json.loads(data) + has_vuln = False + + for adv in advisories: + for vuln in adv.get("vulnerabilities", []): + if vuln.get("package", {}).get("name") != "copyparty": + continue + + patched_str = vuln.get("patched_versions") + if patched_str: + patched_ver = self.parse_version(patched_str) + if ver_cpp < patched_ver: + has_vuln = True + break + if has_vuln: + break + + if has_vuln: + self.broker.say("httpsrv.set_bad_ver", True) + + except Exception as e: + self.log("ver-chk", "failed to process vulnerability advisory; {}".format(e)) + + for _ in range(self.args.vc_interval): + if self.stopping: + return + time.sleep(1) diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 832ef566..5ddcd18f 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -257,3 +257,19 @@ html.bz { html.bz .vols img { filter: sepia(0.8) hue-rotate(180deg); } +.box-warning { + background: #804; + border-bottom: 1px solid #c28; + padding: .75em; + text-align: center; + border-radius: .2em; + margin-top: 0em; +} +.box-warning + .box-warning { + margin-top: .5em; +} +.unbox { + border: unset !important; + padding: unset !important; + margin: unset !important; +} \ No newline at end of file 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\