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
This commit is contained in:
ed 2023-11-03 23:07:16 +00:00
parent 2523d76756
commit 8020b11ea0
7 changed files with 72 additions and 71 deletions

View file

@ -476,12 +476,10 @@ class VFS(object):
err: int = 403, err: int = 403,
) -> tuple["VFS", str]: ) -> tuple["VFS", str]:
"""returns [vfsnode,fs_remainder] if user has the requested permissions""" """returns [vfsnode,fs_remainder] if user has the requested permissions"""
if ANYWIN: if relchk(vpath):
mod = relchk(vpath)
if mod:
if self.log: if self.log:
self.log("vfs", "invalid relpath [{}]".format(vpath)) self.log("vfs", "invalid relpath [{}]".format(vpath))
raise Pebkac(404) raise Pebkac(422)
cvpath = undot(vpath) cvpath = undot(vpath)
vn, rem = self._find(cvpath) vn, rem = self._find(cvpath)

View file

@ -148,7 +148,7 @@ class FtpFs(AbstractedFS):
try: try:
vpath = vpath.replace("\\", "/").strip("/") vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath) rd, fn = os.path.split(vpath)
if ANYWIN and relchk(rd): if relchk(rd):
logging.warning("malicious vpath: %s", vpath) logging.warning("malicious vpath: %s", vpath)
t = "Unsupported characters in [{}]" t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)

View file

@ -37,6 +37,7 @@ from .star import StreamTar
from .sutil import StreamArc, gfilter from .sutil import StreamArc, gfilter
from .szip import StreamZip from .szip import StreamZip
from .util import ( from .util import (
Garda,
HTTPCODE, HTTPCODE,
META_NOBOTS, META_NOBOTS,
MultipartParser, MultipartParser,
@ -75,6 +76,7 @@ from .util import (
runhook, runhook,
s3enc, s3enc,
sanitize_fn, sanitize_fn,
sanitize_vpath,
sendfile_kern, sendfile_kern,
sendfile_py, sendfile_py,
undot, undot,
@ -146,6 +148,7 @@ class HttpCli(object):
self.rem = " " self.rem = " "
self.vpath = " " self.vpath = " "
self.vpaths = " " self.vpaths = " "
self.gctx = " " # additional context for garda
self.trailing_slash = True self.trailing_slash = True
self.uname = " " self.uname = " "
self.pw = " " self.pw = " "
@ -254,8 +257,8 @@ class HttpCli(object):
k, zs = header_line.split(":", 1) k, zs = header_line.split(":", 1)
self.headers[k.lower()] = zs.strip() self.headers[k.lower()] = zs.strip()
except: except:
msg = " ]\n#[ ".join(headerlines) msg = "#[ " + " ]\n#[ ".join(headerlines) + " ]"
raise Pebkac(400, "bad headers:\n#[ " + msg + " ]") raise Pebkac(400, "bad headers", log=msg)
except Pebkac as ex: except Pebkac as ex:
self.mode = "GET" self.mode = "GET"
@ -268,6 +271,10 @@ class HttpCli(object):
self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True) self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True)
except: except:
pass pass
if ex.log:
self.log("additional error context:\n" + ex.log, 6)
return False return False
self.ua = self.headers.get("user-agent", "") 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 self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath
) )
ok = "\x00" not in self.vpath if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"):
if ANYWIN:
ok = ok and not relchk(self.vpath)
if not ok and (self.vpath != "*" or self.mode != "OPTIONS"):
self.log("invalid relpath [{}]".format(self.vpath)) 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 return self.tx_404() and self.keepalive
zso = self.headers.get("authorization") zso = self.headers.get("authorization")
@ -549,6 +553,9 @@ class HttpCli(object):
zb = b"<pre>" + html_escape(msg).encode("utf-8", "replace") zb = b"<pre>" + html_escape(msg).encode("utf-8", "replace")
h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {} h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {}
self.reply(zb, status=pex.code, headers=h, volsan=True) 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 return self.keepalive
except Pebkac: except Pebkac:
return False return False
@ -559,6 +566,34 @@ class HttpCli(object):
else: else:
return self.conn.iphash.s(self.ip) 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: def is_banned(self) -> bool:
if not self.conn.bans: if not self.conn.bans:
return False return False
@ -678,24 +713,7 @@ class HttpCli(object):
or not self.args.nonsus_urls or not self.args.nonsus_urls
or not self.args.nonsus_urls.search(self.vpath) or not self.args.nonsus_urls.search(self.vpath)
): ):
bonk, ip = g.bonk(self.ip, self.vpath) self.cbonk(g, self.vpath, str(status), "%ss" % (status,))
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
if volsan: if volsan:
vols = list(self.asrv.vfs.all_vols.values()) vols = list(self.asrv.vfs.all_vols.values())
@ -2133,27 +2151,7 @@ class HttpCli(object):
logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8") logpwd = "%" + base64.b64encode(zb[:12]).decode("utf-8")
self.log("invalid password: {}".format(logpwd), 3) self.log("invalid password: {}".format(logpwd), 3)
self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
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
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec

View file

@ -1563,8 +1563,8 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
raise Pebkac( raise Pebkac(
400, 400,
"protocol error while reading headers:\n" "protocol error while reading headers",
+ ret.decode("utf-8", "replace"), log=ret.decode("utf-8", "replace"),
) )
ofs = ret.find(b"\r\n\r\n") 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: def relchk(rp: str) -> str:
if "\x00" in rp:
return "[nul]"
if ANYWIN: if ANYWIN:
if "\n" in rp or "\r" in rp: if "\n" in rp or "\r" in rp:
return "x\nx" return "x\nx"
@ -2976,9 +2979,12 @@ def hidedir(dp) -> None:
class Pebkac(Exception): 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]) super(Pebkac, self).__init__(msg or HTTPCODE[code])
self.code = code self.code = code
self.log = log
def __repr__(self) -> str: def __repr__(self) -> str:
return "Pebkac({}, {})".format(self.code, repr(self.args)) return "Pebkac({}, {})".format(self.code, repr(self.args))

View file

@ -3797,7 +3797,7 @@ var fileman = (function () {
function rename_cb() { function rename_cb() {
if (this.status !== 201) { if (this.status !== 201) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fr_efail + msg); toast.err(9, L.fr_efail + msg);
return; return;
} }
@ -3846,7 +3846,7 @@ var fileman = (function () {
} }
function delete_cb() { function delete_cb() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fd_err + msg); toast.err(9, L.fd_err + msg);
return; return;
} }
@ -3967,7 +3967,7 @@ var fileman = (function () {
} }
function paste_cb() { function paste_cb() {
if (this.status !== 201) { if (this.status !== 201) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.fp_err + msg); toast.err(9, L.fp_err + msg);
return; return;
} }
@ -5300,10 +5300,7 @@ document.onkeydown = function (e) {
function xhr_search_results() { function xhr_search_results() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
if (msg.indexOf('<pre>') === 0)
msg = msg.slice(5);
srch_msg(true, "http " + this.status + ": " + msg); srch_msg(true, "http " + this.status + ": " + msg);
search_in_progress = 0; search_in_progress = 0;
return; return;
@ -7194,7 +7191,7 @@ var msel = (function () {
xhrchk(this, L.fd_xe1, L.fd_xe2); xhrchk(this, L.fd_xe1, L.fd_xe2);
if (this.status !== 201) { if (this.status !== 201) {
sf.textContent = 'error: ' + this.responseText; sf.textContent = 'error: ' + unpre(this.responseText);
return; return;
} }
@ -7241,7 +7238,7 @@ var msel = (function () {
xhrchk(this, L.fsm_xe1, L.fsm_xe2); xhrchk(this, L.fsm_xe1, L.fsm_xe2);
if (this.status < 200 || this.status > 201) { if (this.status < 200 || this.status > 201) {
sf.textContent = 'error: ' + this.responseText; sf.textContent = 'error: ' + unpre(this.responseText);
return; return;
} }
@ -7619,7 +7616,7 @@ var unpost = (function () {
function unpost_delete_cb() { function unpost_delete_cb() {
if (this.status !== 200) { if (this.status !== 200) {
var msg = this.responseText; var msg = unpre(this.responseText);
toast.err(9, L.un_derr + msg); toast.err(9, L.un_derr + msg);
return; return;
} }

View file

@ -2402,15 +2402,12 @@ function up2k_init(subtle) {
pvis.seth(t.n, 2, L.u_ehstmp, t); pvis.seth(t.n, 2, L.u_ehstmp, t);
var err = "", var err = "",
rsp = (xhr.responseText + ''), rsp = unpre(this.responseText),
ofs = rsp.lastIndexOf('\nURL: '); ofs = rsp.lastIndexOf('\nURL: ');
if (ofs !== -1) if (ofs !== -1)
rsp = rsp.slice(0, ofs); rsp = rsp.slice(0, ofs);
if (rsp.indexOf('<pre>') === 0)
rsp = rsp.slice(5);
if (rsp.indexOf('rate-limit ') !== -1) { if (rsp.indexOf('rate-limit ') !== -1) {
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0]; var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
console.log("rate-limit: " + penalty); console.log("rate-limit: " + penalty);
@ -2536,7 +2533,7 @@ function up2k_init(subtle) {
cdr = t.size; cdr = t.size;
var orz = function (xhr) { 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) { if (txt.indexOf('upload blocked by x') + 1) {
apop(st.busy.upload, upt); apop(st.busy.upload, upt);
apop(t.postlist, npart); apop(t.postlist, npart);

View file

@ -1356,6 +1356,11 @@ function lf2br(txt) {
} }
function unpre(txt) {
return ('' + txt).replace(/^<pre>/, '');
}
var toast = (function () { var toast = (function () {
var r = {}, var r = {},
te = null, te = null,