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,