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\