mirror of
https://github.com/9001/copyparty.git
synced 2026-04-12 15:22:32 -06:00
fix GHSA-m6hv-x64c-27mm: svg nohtml
This commit is contained in:
parent
981a7cd9dd
commit
1c9f894e14
|
|
@ -2907,7 +2907,9 @@ some notes on hardening
|
|||
* set `--rproxy 0` *if and only if* your copyparty is directly facing the internet (not through a reverse-proxy)
|
||||
* cors doesn't work right otherwise
|
||||
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
|
||||
* this returns html documents as plaintext, and also disables markdown rendering
|
||||
* this returns html documents and svg images as plaintext, and also disables markdown rendering
|
||||
* the `nohtml` volflag also enables `noscript` which, on its own, prevents *most* javascript from running; enabling just `noscript` without `nohtml` makes it probably-safe (see below) to view html and svg files, but `nohtml` is necessary to block javascript in markdown documents
|
||||
* "probably-safe" because it relies on `Content-Security-Policy` so it depends on the reverseproxy to forward it, and the browser to understand it, but `nohtml` (the nuclear option) always works
|
||||
* when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or [`--help-bind`](https://copyparty.eu/cli/#bind-help-page)
|
||||
|
||||
safety profiles:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ def say_no():
|
|||
|
||||
|
||||
def main(cli, vn, rem):
|
||||
cli.send_headers(None, 404, "text/plain")
|
||||
cli.send_headers("oh_f", None, 404, "text/plain")
|
||||
|
||||
for chunk in say_no():
|
||||
cli.s.sendall(chunk)
|
||||
|
|
|
|||
|
|
@ -1074,7 +1074,12 @@ class AuthSrv(object):
|
|||
self.indent = ""
|
||||
self.is_lxc = args.c == ["/z/initcfg"]
|
||||
|
||||
oh = "X-Content-Type-Options: nosniff\r\n"
|
||||
if self.args.http_vary:
|
||||
oh += "Vary: %s\r\n" % (self.args.http_vary,)
|
||||
self._vf0b = {
|
||||
"oh_g": oh + "\r\n",
|
||||
"oh_f": oh + "\r\n",
|
||||
"cachectl": self.args.cachectl,
|
||||
"tcolor": self.args.tcolor,
|
||||
"du_iwho": self.args.du_iwho,
|
||||
|
|
@ -2634,8 +2639,17 @@ class AuthSrv(object):
|
|||
if head_s and not head_s.endswith("\n"):
|
||||
head_s += "\n"
|
||||
|
||||
zs = "X-Content-Type-Options: nosniff\r\n"
|
||||
if "norobots" in vol.flags:
|
||||
head_s += META_NOBOTS
|
||||
zs += "X-Robots-Tag: noindex, nofollow\r\n"
|
||||
if self.args.http_vary:
|
||||
zs += "Vary: %s\r\n" % (self.args.http_vary,)
|
||||
vol.flags["oh_g"] = zs + "\r\n"
|
||||
|
||||
if "noscript" in vol.flags:
|
||||
zs += "Content-Security-Policy: script-src 'none';\r\n"
|
||||
vol.flags["oh_f"] = zs + "\r\n"
|
||||
|
||||
ico_url = vol.flags.get("ufavico")
|
||||
if ico_url:
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ flagcats = {
|
|||
"md_sba": "value of iframe allow-prop for markdown-sandbox",
|
||||
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
|
||||
"nohtml": "return html and markdown as text/html",
|
||||
"noscript": "disable most javascript by use of CSP",
|
||||
"ui_noacci": "hide account-info in the UI",
|
||||
"ui_nocpla": "hide cpanel-link in the UI",
|
||||
"ui_nolbar": "hide link-bar in the UI",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from .util import (
|
|||
E_SCK_WR,
|
||||
HAVE_SQLITE3,
|
||||
HTTPCODE,
|
||||
SAFE_MIMES,
|
||||
UTC,
|
||||
VPTL_MAC,
|
||||
VPTL_OS,
|
||||
|
|
@ -105,6 +106,7 @@ from .util import (
|
|||
runhook,
|
||||
s2hms,
|
||||
s3enc,
|
||||
safe_mime,
|
||||
sanitize_fn,
|
||||
sanitize_vpath,
|
||||
sendfile_kern,
|
||||
|
|
@ -332,7 +334,6 @@ class HttpCli(object):
|
|||
def run(self) -> bool:
|
||||
"""returns true if connection can be reused"""
|
||||
self.out_headers = {
|
||||
"Vary": self.args.http_vary,
|
||||
"Cache-Control": "no-store, max-age=0",
|
||||
}
|
||||
|
||||
|
|
@ -855,9 +856,6 @@ class HttpCli(object):
|
|||
|
||||
self.s.settimeout(self.args.s_tbody or None)
|
||||
|
||||
if "norobots" in vn.flags:
|
||||
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
|
||||
if "html_head_s" in vn.flags:
|
||||
self.html_head += vn.flags["html_head_s"]
|
||||
|
||||
|
|
@ -1072,6 +1070,7 @@ class HttpCli(object):
|
|||
|
||||
def send_headers(
|
||||
self,
|
||||
oh_k: str,
|
||||
length: Optional[int],
|
||||
status: int = 200,
|
||||
mime: Optional[str] = None,
|
||||
|
|
@ -1114,7 +1113,11 @@ class HttpCli(object):
|
|||
self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
|
||||
raise Pebkac(999)
|
||||
|
||||
response.append(self.vn.flags[oh_k])
|
||||
|
||||
if self.args.ohead and self.do_log:
|
||||
zs = response.pop()[:-4]
|
||||
response.extend(zs.split("\r\n"))
|
||||
keys = self.args.ohead
|
||||
if "*" in keys:
|
||||
lines = response[1:]
|
||||
|
|
@ -1126,8 +1129,8 @@ class HttpCli(object):
|
|||
for zs in lines:
|
||||
hk, hv = zs.split(": ")
|
||||
self.log("[O] {}: \033[33m[{}]".format(hk, hv), 5)
|
||||
response.append("\r\n")
|
||||
|
||||
response.append("\r\n")
|
||||
try:
|
||||
self.s.sendall("\r\n".join(response).encode("utf-8"))
|
||||
except:
|
||||
|
|
@ -1184,7 +1187,7 @@ class HttpCli(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
self.send_headers(len(body), status, mime, headers)
|
||||
self.send_headers("oh_g", len(body), status, mime, headers)
|
||||
|
||||
try:
|
||||
if self.mode != "HEAD":
|
||||
|
|
@ -1389,7 +1392,7 @@ class HttpCli(object):
|
|||
if res_path in RES:
|
||||
ap = self.E.mod_ + res_path
|
||||
if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
|
||||
return self.tx_file(ap)
|
||||
return self.tx_file("oh_g", ap)
|
||||
else:
|
||||
return self.tx_res(res_path)
|
||||
|
||||
|
|
@ -1403,7 +1406,7 @@ class HttpCli(object):
|
|||
# return mimetype matching request extension
|
||||
self.ouparam["dl"] = res_path.split("/")[-1]
|
||||
if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
|
||||
return self.tx_file(ap)
|
||||
return self.tx_file("oh_g", ap)
|
||||
else:
|
||||
return self.tx_res(res_path)
|
||||
|
||||
|
|
@ -1717,7 +1720,10 @@ class HttpCli(object):
|
|||
if zi.file_size >= maxsz:
|
||||
raise Pebkac(404, "zip bomb defused")
|
||||
with zf.open(zi, "r") as fi:
|
||||
self.send_headers(length=zi.file_size, mime=guess_mime(inner_path))
|
||||
mime = guess_mime(inner_path)
|
||||
if mime not in SAFE_MIMES and "nohtml" in self.vn.flags:
|
||||
mime = safe_mime(mime)
|
||||
self.send_headers("oh_f", length=zi.file_size, mime=mime)
|
||||
|
||||
sendfile_py(
|
||||
self.log,
|
||||
|
|
@ -1913,7 +1919,11 @@ class HttpCli(object):
|
|||
chunksz = 0x7FF8 # preferred by nginx or cf (dunno which)
|
||||
|
||||
self.send_headers(
|
||||
None, 207, "text/xml; charset=" + enc, {"Transfer-Encoding": "chunked"}
|
||||
"oh_f",
|
||||
None,
|
||||
207,
|
||||
"text/xml; charset=" + enc,
|
||||
{"Transfer-Encoding": "chunked"},
|
||||
)
|
||||
|
||||
ap = ""
|
||||
|
|
@ -2120,7 +2130,7 @@ class HttpCli(object):
|
|||
self.log("%s tried to lock %r" % (self.uname, "/" + self.vpath))
|
||||
raise Pebkac(401, "authenticate")
|
||||
|
||||
self.send_headers(None, 204)
|
||||
self.send_headers("oh_f", None, 204)
|
||||
return True
|
||||
|
||||
def handle_mkcol(self) -> bool:
|
||||
|
|
@ -2222,7 +2232,7 @@ class HttpCli(object):
|
|||
oh["Ms-Author-Via"] = "DAV"
|
||||
|
||||
# winxp-webdav doesnt know what 204 is
|
||||
self.send_headers(0, 200)
|
||||
self.send_headers("oh_f", 0, 200)
|
||||
return True
|
||||
|
||||
def handle_delete(self) -> bool:
|
||||
|
|
@ -4525,11 +4535,11 @@ class HttpCli(object):
|
|||
if self.do_log:
|
||||
self.log(logmsg)
|
||||
|
||||
self.send_headers(length=file_sz, status=status, mime=mime)
|
||||
self.send_headers("oh_g", length=file_sz, status=status, mime=mime)
|
||||
return True
|
||||
|
||||
ret = True
|
||||
self.send_headers(length=file_sz, status=status, mime=mime)
|
||||
self.send_headers("oh_g", length=file_sz, status=status, mime=mime)
|
||||
remains = sendfile_py(
|
||||
self.log,
|
||||
0,
|
||||
|
|
@ -4554,7 +4564,7 @@ class HttpCli(object):
|
|||
|
||||
return ret
|
||||
|
||||
def tx_file(self, req_path: str, ptop: Optional[str] = None) -> bool:
|
||||
def tx_file(self, oh_k: str, req_path: str, ptop: Optional[str] = None) -> bool:
|
||||
status = 200
|
||||
logmsg = "{:4} {} ".format("", self.req)
|
||||
logtail = ""
|
||||
|
|
@ -4757,8 +4767,8 @@ class HttpCli(object):
|
|||
else:
|
||||
mime = guess_mime(cdis)
|
||||
|
||||
if "nohtml" in self.vn.flags and "html" in mime:
|
||||
mime = "text/plain; charset=utf-8"
|
||||
if mime not in SAFE_MIMES and "nohtml" in self.vn.flags:
|
||||
mime = safe_mime(mime)
|
||||
|
||||
self.out_headers["Accept-Ranges"] = "bytes"
|
||||
logmsg += unicode(status) + logtail
|
||||
|
|
@ -4767,7 +4777,7 @@ class HttpCli(object):
|
|||
if self.do_log:
|
||||
self.log(logmsg)
|
||||
|
||||
self.send_headers(length=upper - lower, status=status, mime=mime)
|
||||
self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)
|
||||
return True
|
||||
|
||||
dls = self.conn.hsrv.dls
|
||||
|
|
@ -4799,7 +4809,7 @@ class HttpCli(object):
|
|||
|
||||
ret = True
|
||||
with open_func(*open_args) as f:
|
||||
self.send_headers(length=upper - lower, status=status, mime=mime)
|
||||
self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)
|
||||
|
||||
sendfun = sendfile_kern if use_sendfile else sendfile_py
|
||||
remains = sendfun(
|
||||
|
|
@ -4832,7 +4842,7 @@ class HttpCli(object):
|
|||
mime: str,
|
||||
) -> None:
|
||||
vf = self.vn.flags
|
||||
self.send_headers(length=None, status=status, mime=mime)
|
||||
self.send_headers("oh_f", length=None, status=status, mime=mime)
|
||||
abspath: bytes = open_args[0]
|
||||
sec_rate = vf["tail_rate"]
|
||||
sec_max = vf["tail_tmax"]
|
||||
|
|
@ -4972,7 +4982,7 @@ class HttpCli(object):
|
|||
logmsg: str,
|
||||
) -> bool:
|
||||
M = 1048576
|
||||
self.send_headers(length=upper - lower, status=status, mime=mime)
|
||||
self.send_headers("oh_f", length=upper - lower, status=status, mime=mime)
|
||||
wr_slp = self.args.s_wr_slp
|
||||
wr_sz = self.args.s_wr_sz
|
||||
file_size = job["size"]
|
||||
|
|
@ -5185,7 +5195,9 @@ class HttpCli(object):
|
|||
|
||||
cdis = gen_content_disposition("%s.%s" % (fn, ext))
|
||||
self.log(repr(cdis))
|
||||
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
|
||||
self.send_headers(
|
||||
"oh_f", None, mime=mime, headers={"Content-Disposition": cdis}
|
||||
)
|
||||
|
||||
fgen = vn.zipgen(vpath, rem, set(items), self.uname, False, dots, scandir)
|
||||
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
|
||||
|
|
@ -5393,7 +5405,7 @@ class HttpCli(object):
|
|||
if len(html) != 2:
|
||||
raise Exception("boundary appears in " + tpl)
|
||||
|
||||
self.send_headers(sz_md + len(html[0]) + len(html[1]), status)
|
||||
self.send_headers("oh_g", sz_md + len(html[0]) + len(html[1]), status)
|
||||
|
||||
logmsg += unicode(status)
|
||||
if self.mode == "HEAD" or not do_send:
|
||||
|
|
@ -6766,7 +6778,7 @@ class HttpCli(object):
|
|||
add_og = True
|
||||
og_fn = ""
|
||||
|
||||
if "b" in self.uparam:
|
||||
if "b" in self.uparam and "norobots" not in vn.flags:
|
||||
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
|
|
@ -6838,7 +6850,7 @@ class HttpCli(object):
|
|||
raise
|
||||
|
||||
if thp:
|
||||
return self.tx_file(thp)
|
||||
return self.tx_file("oh_f", thp)
|
||||
|
||||
if th_fmt == "p":
|
||||
raise Pebkac(404)
|
||||
|
|
@ -6927,9 +6939,9 @@ class HttpCli(object):
|
|||
|
||||
if not add_og or not og_fn:
|
||||
if st.st_size or "nopipe" in vn.flags:
|
||||
return self.tx_file(abspath, None)
|
||||
return self.tx_file("oh_f", abspath, None)
|
||||
else:
|
||||
return self.tx_file(abspath, vn.get_dbv("")[0].realpath)
|
||||
return self.tx_file("oh_f", abspath, vn.get_dbv("")[0].realpath)
|
||||
|
||||
elif is_dir and not self.can_read:
|
||||
if use_dirkey:
|
||||
|
|
@ -7277,7 +7289,7 @@ class HttpCli(object):
|
|||
return self.redirect(
|
||||
self.vpath + "/", flavor="redirecting to", use302=True
|
||||
)
|
||||
return self.tx_file(ap) # is no-cache
|
||||
return self.tx_file("oh_f", ap) # is no-cache
|
||||
|
||||
if icur:
|
||||
mte = vn.flags.get("mte") or {}
|
||||
|
|
|
|||
|
|
@ -1154,7 +1154,7 @@ class Up2k(object):
|
|||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t rw_edit_set srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
|
|
|
|||
|
|
@ -413,6 +413,7 @@ IMPLICATIONS = [
|
|||
["tftpvv", "tftpv"],
|
||||
["nodupem", "nodupe"],
|
||||
["no_dupe_m", "no_dupe"],
|
||||
["nohtml", "noscript"],
|
||||
["sftpvv", "sftpv"],
|
||||
["smbw", "smb"],
|
||||
["smb1", "smb"],
|
||||
|
|
@ -474,7 +475,7 @@ MIMES = {
|
|||
}
|
||||
|
||||
|
||||
def _add_mimes() -> None:
|
||||
def _add_mimes() -> set[str]:
|
||||
# `mimetypes` is woefully unpopulated on windows
|
||||
# but will be used as fallback on linux
|
||||
|
||||
|
|
@ -511,8 +512,11 @@ font ttc=collection
|
|||
ext, mime = em.split("=")
|
||||
MIMES[ext] = "{}/{}".format(k, mime)
|
||||
|
||||
ptn = re.compile("html|script|tension|wasm|xml")
|
||||
return {x for x in MIMES.values() if not ptn.search(x)}
|
||||
|
||||
_add_mimes()
|
||||
|
||||
SAFE_MIMES = _add_mimes()
|
||||
|
||||
|
||||
EXTS: dict[str, str] = {v: k for k, v in MIMES.items()}
|
||||
|
|
@ -3503,6 +3507,13 @@ def guess_mime(
|
|||
return ret
|
||||
|
||||
|
||||
def safe_mime(mime: str) -> str:
|
||||
if "text/" in mime or "xml" in mime:
|
||||
return "text/plain; charset=utf-8"
|
||||
else:
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
def getalive(pids: list[int], pgid: int) -> list[int]:
|
||||
alive = []
|
||||
for pid in pids:
|
||||
|
|
|
|||
Loading…
Reference in a new issue