From 8020b11ea0d98f1234cb8c0156482e2ccf20118a Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 3 Nov 2023 23:07:16 +0000 Subject: [PATCH] improve/simplify validation/errorhandling: * some malicious requests are now answered with HTTP 422, so that they count against --ban-422 * do not include request headers when replying to invalid requests, in case there is a reverse-proxy inserting something interesting --- copyparty/authsrv.py | 10 ++--- copyparty/ftpd.py | 2 +- copyparty/httpcli.py | 90 ++++++++++++++++++++-------------------- copyparty/util.py | 12 ++++-- copyparty/web/browser.js | 17 ++++---- copyparty/web/up2k.js | 7 +--- copyparty/web/util.js | 5 +++ 7 files changed, 72 insertions(+), 71 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 29e8fca7..c20c9d09 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -476,12 +476,10 @@ class VFS(object): err: int = 403, ) -> tuple["VFS", str]: """returns [vfsnode,fs_remainder] if user has the requested permissions""" - if ANYWIN: - mod = relchk(vpath) - if mod: - if self.log: - self.log("vfs", "invalid relpath [{}]".format(vpath)) - raise Pebkac(404) + if relchk(vpath): + if self.log: + self.log("vfs", "invalid relpath [{}]".format(vpath)) + raise Pebkac(422) cvpath = undot(vpath) vn, rem = self._find(cvpath) diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index d691c8fc..cb0573bb 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -148,7 +148,7 @@ class FtpFs(AbstractedFS): try: vpath = vpath.replace("\\", "/").strip("/") rd, fn = os.path.split(vpath) - if ANYWIN and relchk(rd): + if relchk(rd): logging.warning("malicious vpath: %s", vpath) t = "Unsupported characters in [{}]" raise FSE(t.format(vpath), 1) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8879826d..23c816da 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -37,6 +37,7 @@ from .star import StreamTar from .sutil import StreamArc, gfilter from .szip import StreamZip from .util import ( + Garda, HTTPCODE, META_NOBOTS, MultipartParser, @@ -75,6 +76,7 @@ from .util import ( runhook, s3enc, sanitize_fn, + sanitize_vpath, sendfile_kern, sendfile_py, undot, @@ -146,6 +148,7 @@ class HttpCli(object): self.rem = " " self.vpath = " " self.vpaths = " " + self.gctx = " " # additional context for garda self.trailing_slash = True self.uname = " " self.pw = " " @@ -254,8 +257,8 @@ class HttpCli(object): k, zs = header_line.split(":", 1) self.headers[k.lower()] = zs.strip() except: - msg = " ]\n#[ ".join(headerlines) - raise Pebkac(400, "bad headers:\n#[ " + msg + " ]") + msg = "#[ " + " ]\n#[ ".join(headerlines) + " ]" + raise Pebkac(400, "bad headers", log=msg) except Pebkac as ex: self.mode = "GET" @@ -268,6 +271,10 @@ class HttpCli(object): self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True) except: pass + + if ex.log: + self.log("additional error context:\n" + ex.log, 6) + return False self.ua = self.headers.get("user-agent", "") @@ -411,12 +418,9 @@ class HttpCli(object): self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath ) - ok = "\x00" not in self.vpath - if ANYWIN: - ok = ok and not relchk(self.vpath) - - if not ok and (self.vpath != "*" or self.mode != "OPTIONS"): + if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"): self.log("invalid relpath [{}]".format(self.vpath)) + self.cbonk(self.conn.hsrv.g422, self.vpath, "bad_vp", "invalid relpaths") return self.tx_404() and self.keepalive zso = self.headers.get("authorization") @@ -549,6 +553,9 @@ class HttpCli(object): zb = b"
" + html_escape(msg).encode("utf-8", "replace")
                 h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {}
                 self.reply(zb, status=pex.code, headers=h, volsan=True)
+                if pex.log:
+                    self.log("additional error context:\n" + pex.log, 6)
+
                 return self.keepalive
             except Pebkac:
                 return False
@@ -559,6 +566,34 @@ class HttpCli(object):
         else:
             return self.conn.iphash.s(self.ip)
 
+    def cbonk(self, g: Garda, v: str, reason: str, descr: str) -> bool:
+        if not g.lim:
+            return False
+
+        bonk, ip = g.bonk(self.ip, v + self.gctx)
+        if not bonk:
+            return False
+
+        xban = self.vn.flags.get("xban")
+        if not xban or not runhook(
+            self.log,
+            xban,
+            self.vn.canonical(self.rem),
+            self.vpath,
+            self.host,
+            self.uname,
+            time.time(),
+            0,
+            self.ip,
+            time.time(),
+            reason,
+        ):
+            self.log("client banned: %s" % (descr,), 1)
+            self.conn.hsrv.bans[ip] = bonk
+            return True
+
+        return False
+
     def is_banned(self) -> bool:
         if not self.conn.bans:
             return False
@@ -678,24 +713,7 @@ class HttpCli(object):
                 or not self.args.nonsus_urls
                 or not self.args.nonsus_urls.search(self.vpath)
             ):
-                bonk, ip = g.bonk(self.ip, self.vpath)
-                if bonk:
-                    xban = self.vn.flags.get("xban")
-                    if not xban or not runhook(
-                        self.log,
-                        xban,
-                        self.vn.canonical(self.rem),
-                        self.vpath,
-                        self.host,
-                        self.uname,
-                        time.time(),
-                        0,
-                        self.ip,
-                        time.time(),
-                        str(status),
-                    ):
-                        self.log("client banned: %ss" % (status,), 1)
-                        self.conn.hsrv.bans[ip] = bonk
+                self.cbonk(g, self.vpath, str(status), "%ss" % (status,))
 
         if volsan:
             vols = list(self.asrv.vfs.all_vols.values())
@@ -2133,27 +2151,7 @@ class HttpCli(object):
                 logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8")
 
             self.log("invalid password: {}".format(logpwd), 3)
-
-            g = self.conn.hsrv.gpwd
-            if g.lim:
-                bonk, ip = g.bonk(self.ip, pwd)
-                if bonk:
-                    xban = self.vn.flags.get("xban")
-                    if not xban or not runhook(
-                        self.log,
-                        xban,
-                        self.vn.canonical(self.rem),
-                        self.vpath,
-                        self.host,
-                        self.uname,
-                        time.time(),
-                        0,
-                        self.ip,
-                        time.time(),
-                        "pw",
-                    ):
-                        self.log("client banned: invalid passwords", 1)
-                        self.conn.hsrv.bans[ip] = bonk
+            self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
 
             msg = "naw dude"
             pwd = "x"  # nosec
diff --git a/copyparty/util.py b/copyparty/util.py
index 26133374..d2ea0db2 100644
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -1563,8 +1563,8 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
 
             raise Pebkac(
                 400,
-                "protocol error while reading headers:\n"
-                + ret.decode("utf-8", "replace"),
+                "protocol error while reading headers",
+                log=ret.decode("utf-8", "replace"),
             )
 
         ofs = ret.find(b"\r\n\r\n")
@@ -1774,6 +1774,9 @@ def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
 
 
 def relchk(rp: str) -> str:
+    if "\x00" in rp:
+        return "[nul]"
+
     if ANYWIN:
         if "\n" in rp or "\r" in rp:
             return "x\nx"
@@ -2976,9 +2979,12 @@ def hidedir(dp) -> None:
 
 
 class Pebkac(Exception):
-    def __init__(self, code: int, msg: Optional[str] = None) -> None:
+    def __init__(
+        self, code: int, msg: Optional[str] = None, log: Optional[str] = None
+    ) -> None:
         super(Pebkac, self).__init__(msg or HTTPCODE[code])
         self.code = code
+        self.log = log
 
     def __repr__(self) -> str:
         return "Pebkac({}, {})".format(self.code, repr(self.args))
diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js
index 4fb0b6aa..878a5f73 100644
--- a/copyparty/web/browser.js
+++ b/copyparty/web/browser.js
@@ -3797,7 +3797,7 @@ var fileman = (function () {
 
 			function rename_cb() {
 				if (this.status !== 201) {
-					var msg = this.responseText;
+					var msg = unpre(this.responseText);
 					toast.err(9, L.fr_efail + msg);
 					return;
 				}
@@ -3846,7 +3846,7 @@ var fileman = (function () {
 		}
 		function delete_cb() {
 			if (this.status !== 200) {
-				var msg = this.responseText;
+				var msg = unpre(this.responseText);
 				toast.err(9, L.fd_err + msg);
 				return;
 			}
@@ -3967,7 +3967,7 @@ var fileman = (function () {
 		}
 		function paste_cb() {
 			if (this.status !== 201) {
-				var msg = this.responseText;
+				var msg = unpre(this.responseText);
 				toast.err(9, L.fp_err + msg);
 				return;
 			}
@@ -5300,10 +5300,7 @@ document.onkeydown = function (e) {
 
 	function xhr_search_results() {
 		if (this.status !== 200) {
-			var msg = this.responseText;
-			if (msg.indexOf('
') === 0)
-				msg = msg.slice(5);
-
+			var msg = unpre(this.responseText);
 			srch_msg(true, "http " + this.status + ": " + msg);
 			search_in_progress = 0;
 			return;
@@ -7194,7 +7191,7 @@ var msel = (function () {
 		xhrchk(this, L.fd_xe1, L.fd_xe2);
 
 		if (this.status !== 201) {
-			sf.textContent = 'error: ' + this.responseText;
+			sf.textContent = 'error: ' + unpre(this.responseText);
 			return;
 		}
 
@@ -7241,7 +7238,7 @@ var msel = (function () {
 		xhrchk(this, L.fsm_xe1, L.fsm_xe2);
 
 		if (this.status < 200 || this.status > 201) {
-			sf.textContent = 'error: ' + this.responseText;
+			sf.textContent = 'error: ' + unpre(this.responseText);
 			return;
 		}
 
@@ -7619,7 +7616,7 @@ var unpost = (function () {
 
 	function unpost_delete_cb() {
 		if (this.status !== 200) {
-			var msg = this.responseText;
+			var msg = unpre(this.responseText);
 			toast.err(9, L.un_derr + msg);
 			return;
 		}
diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js
index 3448c397..f9a13845 100644
--- a/copyparty/web/up2k.js
+++ b/copyparty/web/up2k.js
@@ -2402,15 +2402,12 @@ function up2k_init(subtle) {
                 pvis.seth(t.n, 2, L.u_ehstmp, t);
 
                 var err = "",
-                    rsp = (xhr.responseText + ''),
+                    rsp = unpre(this.responseText),
                     ofs = rsp.lastIndexOf('\nURL: ');
 
                 if (ofs !== -1)
                     rsp = rsp.slice(0, ofs);
 
-                if (rsp.indexOf('
') === 0)
-                    rsp = rsp.slice(5);
-
                 if (rsp.indexOf('rate-limit ') !== -1) {
                     var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
                     console.log("rate-limit: " + penalty);
@@ -2536,7 +2533,7 @@ function up2k_init(subtle) {
             cdr = t.size;
 
         var orz = function (xhr) {
-            var txt = ((xhr.response && xhr.response.err) || xhr.responseText) + '';
+            var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
             if (txt.indexOf('upload blocked by x') + 1) {
                 apop(st.busy.upload, upt);
                 apop(t.postlist, npart);
diff --git a/copyparty/web/util.js b/copyparty/web/util.js
index 7082264f..738fc54e 100644
--- a/copyparty/web/util.js
+++ b/copyparty/web/util.js
@@ -1356,6 +1356,11 @@ function lf2br(txt) {
 }
 
 
+function unpre(txt) {
+    return ('' + txt).replace(/^
/, '');
+}
+
+
 var toast = (function () {
     var r = {},
         te = null,