volflag "nohtml" to never return html or rendered markdown from potentially unsafe volumes

This commit is contained in:
ed 2023-07-13 21:57:52 +00:00
parent 551d99b71b
commit 50c7bba6ea
3 changed files with 25 additions and 18 deletions

View file

@ -1537,6 +1537,7 @@ some notes on hardening
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy) * set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
* cors doesn't work right otherwise * 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`
safety profiles: safety profiles:

View file

@ -155,6 +155,7 @@ flagcats = {
"sb_lg": "enable js sandbox for prologue/epilogue (default)", "sb_lg": "enable js sandbox for prologue/epilogue (default)",
"md_sbf": "list of markdown-sandbox safeguards to disable", "md_sbf": "list of markdown-sandbox safeguards to disable",
"lg_sbf": "list of *logue-sandbox safeguards to disable", "lg_sbf": "list of *logue-sandbox safeguards to disable",
"nohtml": "return html and markdown as text/html",
}, },
"others": { "others": {
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission', "fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',

View file

@ -137,6 +137,8 @@ class HttpCli(object):
self.uparam: dict[str, str] = {} self.uparam: dict[str, str] = {}
self.cookies: dict[str, str] = {} self.cookies: dict[str, str] = {}
self.avn: Optional[VFS] = None self.avn: Optional[VFS] = None
self.vn = self.asrv.vfs
self.rem = " "
self.vpath = " " self.vpath = " "
self.uname = " " self.uname = " "
self.pw = " " self.pw = " "
@ -437,6 +439,8 @@ class HttpCli(object):
avn.can_access("", self.uname) if avn else [False] * 6 avn.can_access("", self.uname) if avn else [False] * 6
) )
self.avn = avn self.avn = avn
self.vn = vn
self.rem = rem
self.s.settimeout(self.args.s_tbody or None) self.s.settimeout(self.args.s_tbody or None)
@ -572,9 +576,8 @@ class HttpCli(object):
# default to utf8 html if no content-type is set # default to utf8 html if no content-type is set
if not mime: if not mime:
mime = self.out_headers.get("Content-Type", "text/html; charset=utf-8") mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
assert mime
self.out_headers["Content-Type"] = mime self.out_headers["Content-Type"] = mime
for k, zs in list(self.out_headers.items()) + self.out_headerlist: for k, zs in list(self.out_headers.items()) + self.out_headerlist:
@ -773,9 +776,8 @@ class HttpCli(object):
t = "@{} has no access to [{}]" t = "@{} has no access to [{}]"
self.log(t.format(self.uname, self.vpath)) self.log(t.format(self.uname, self.vpath))
if self.avn and "on403" in self.avn.flags: if "on403" in self.vn.flags:
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
ret = self.on40x(vn.flags["on403"], vn, rem)
if ret == "true": if ret == "true":
return True return True
elif ret == "false": elif ret == "false":
@ -1042,9 +1044,6 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml from .dxml import mkenod, mktnod, parse_xml
self.asrv.vfs.get(self.vpath, self.uname, False, False)
# abspath = vn.dcanonical(rem)
buf = b"" buf = b""
for rbuf in self.get_body_reader()[0]: for rbuf in self.get_body_reader()[0]:
buf += rbuf buf += rbuf
@ -1101,8 +1100,7 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml from .dxml import mkenod, mktnod, parse_xml
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) abspath = self.vn.dcanonical(self.rem)
abspath = vn.dcanonical(rem)
buf = b"" buf = b""
for rbuf in self.get_body_reader()[0]: for rbuf in self.get_body_reader()[0]:
@ -1316,15 +1314,12 @@ class HttpCli(object):
plain = zb.decode("utf-8", "replace") plain = zb.decode("utf-8", "replace")
if buf.startswith(b"msg="): if buf.startswith(b"msg="):
plain = plain[4:] plain = plain[4:]
vfs, rem = self.asrv.vfs.get( xm = self.vn.flags.get("xm")
self.vpath, self.uname, False, False
)
xm = vfs.flags.get("xm")
if xm: if xm:
runhook( runhook(
self.log, self.log,
xm, xm,
vfs.canonical(rem), self.vn.canonical(self.rem),
self.vpath, self.vpath,
self.host, self.host,
self.uname, self.uname,
@ -2731,6 +2726,9 @@ class HttpCli(object):
else: else:
mime = guess_mime(req_path) mime = guess_mime(req_path)
if "nohtml" in self.vn.flags and "html" in mime:
mime = "text/plain; charset=utf-8"
self.out_headers["Accept-Ranges"] = "bytes" self.out_headers["Accept-Ranges"] = "bytes"
self.send_headers(length=upper - lower, status=status, mime=mime) self.send_headers(length=upper - lower, status=status, mime=mime)
@ -3400,7 +3398,8 @@ class HttpCli(object):
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) vn = self.vn
rem = self.rem
abspath = vn.dcanonical(rem) abspath = vn.dcanonical(rem)
dbv, vrem = vn.get_dbv(rem) dbv, vrem = vn.get_dbv(rem)
@ -3496,8 +3495,14 @@ class HttpCli(object):
self.log("wrong filekey, want {}, got {}".format(correct, got)) self.log("wrong filekey, want {}, got {}".format(correct, got))
return self.tx_404() return self.tx_404()
if abspath.endswith(".md") and ( if (
"v" in self.uparam or "edit" in self.uparam or "edit2" in self.uparam abspath.endswith(".md")
and "nohtml" not in vn.flags
and (
"v" in self.uparam
or "edit" in self.uparam
or "edit2" in self.uparam
)
): ):
return self.tx_md(abspath) return self.tx_md(abspath)