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,