windows: restrict tool paths;

the warning in commit bb40804f2d is still true, but
this makes copyparty slightly safer in environments
where hostile binaries may appear in CWD
This commit is contained in:
ed 2026-07-27 17:34:52 +00:00
parent bc45299b86
commit cea97ac6fd
5 changed files with 49 additions and 21 deletions

View file

@ -1727,6 +1727,8 @@ def add_safety(ap):
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
if FULL_HELP or ANYWIN:
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
if FULL_HELP or (not ANYWIN and not UNIX):
ap2.add_argument("--th-bwrap", metavar="CMD", type=u, default=th_bwrap, help="optional bwrap sandbox command for FFmpeg and dcraw (Linux-only)")
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help="a/n/f; [\033[32ma\033[0m]=auto (yes if the program 'bwrap' exists (assumes it works)), [\033[32mn\033[0m]=no (assumes bwrap is broken), [\033[32mf\033[0m]=force (disables FFmpeg if bwrap unavailable)")

View file

@ -17,7 +17,9 @@ from .util import (
FFMPEG_URL,
LOG,
REKOBO_LKEY,
SCWD,
VF_CAREFUL,
absreal,
expand_osenv_c,
fsenc,
gzip,
@ -47,10 +49,10 @@ except:
HAVE_MUTAGEN = False
def have_ff(name: str) -> bytes:
def have_ff(name: str) -> list[bytes]:
uname = name.upper()
if os.environ.get("PRTY_NO_" + uname):
return b""
return []
ebin = os.environ.get("PRTY_%s_BIN" % (uname,))
try:
@ -66,11 +68,12 @@ def have_ff(name: str) -> bytes:
bcmd = scmd.encode("utf-8")
try:
sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate()
return bcmd
return [bcmd]
except:
return b""
return []
else:
return (shutil.which(scmd) or "").encode("utf-8")
ret = shutil.which(scmd)
return [absreal(ret).encode("utf-8")] if ret else []
HAVE_FFMPEG = have_ff("ffmpeg")
@ -247,14 +250,14 @@ def ffprobe(
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
# ffprobe -hide_banner -show_streams -show_format --
bap = fsenc(abspath)
cmd = bwrap(HAVE_FFPROBE, bap, b"") + [
cmd = bwrap(HAVE_FFPROBE[0], bap, b"") + [
b"-hide_banner",
b"-show_streams",
b"-show_format",
b"--",
bap,
]
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
rc, so, se = runcmd(cmd, cwd=SCWD, timeout=timeout, nice=True, oom=200)
if rc and TH_BWRAP:
bwrap_fail(se)
retchk(rc, cmd, se)

View file

@ -81,6 +81,7 @@ from .util import (
alltrace,
build_netmap,
expat_ver,
fsenc,
gzip,
html_escape,
load_ipr,
@ -400,6 +401,9 @@ class SvcHub(object):
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
ramdisk_chk(self.asrv)
if ANYWIN and not self.args.unsafe_tools:
self._check_toolpaths()
if args.cgen:
self.asrv.cgen()
@ -1025,6 +1029,18 @@ class SvcHub(object):
if zb:
Daemon(self.s6_notify, "s6-notify", (zb,))
def _check_toolpaths(self) -> None:
for vol in self.asrv.vfs.all_vols.values():
if not vol.realpath or not vol.axs.uwrite:
continue
ap_vol = (vol.realpath + os.sep).encode("utf-8")
for zsl in (HAVE_FFMPEG, HAVE_FFPROBE, HAVE_DCRAW):
if zsl and zsl[0].startswith(ap_vol):
zs = zsl[0].decode("utf-8", "replace")
t = "will not use [%s] because it is inside a writable volume [/%s] => [%s] and --unsafe-tools is not enabled"
self.log("root", t % (zs, vol.vpath, vol.realpath), 3)
del zsl[0]
def _feature_test(self) -> None:
fok = []
fng = []

View file

@ -31,6 +31,7 @@ from .mtag import (
from .util import BytesIO # type: ignore
from .util import (
FFMPEG_URL,
SCWD,
VF_CAREFUL,
Cooldown,
Daemon,
@ -774,7 +775,7 @@ class ThumbSrv(object):
self.wait4ram(0.6, tpath)
bap = fsenc(abspath)
# fmt: off
cmd = bwrap(HAVE_DCRAW, bap, b"") + [
cmd = bwrap(HAVE_DCRAW[0], bap, b"") + [
b"-h", # halfsize
b"-o", b"1", # srgb
b"-s", b"0", # first frame
@ -782,7 +783,7 @@ class ThumbSrv(object):
bap,
]
# fmt: on
p = sp.Popen(cmd, stdout=sp.PIPE)
p = sp.Popen(cmd, stdout=sp.PIPE, cwd=SCWD)
try:
if HAVE_PIL:
self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn)
@ -877,7 +878,7 @@ class ThumbSrv(object):
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner"
@ -918,7 +919,7 @@ class ThumbSrv(object):
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
ret, _, serr = runcmd(cmd, cwd=SCWD, timeout=vn.flags[kto], nice=True, oom=oom)
if not ret:
return
@ -1020,7 +1021,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1101,7 +1102,7 @@ class ThumbSrv(object):
bap_out = fsenc(infile)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1135,7 +1136,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1173,7 +1174,7 @@ class ThumbSrv(object):
# to not support opus then it's probably also super picky
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1204,7 +1205,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1241,7 +1242,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1304,7 +1305,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1347,7 +1348,7 @@ class ThumbSrv(object):
bap_out = fsenc(tmp_opus)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1374,7 +1375,7 @@ class ThumbSrv(object):
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1396,7 +1397,7 @@ class ThumbSrv(object):
bap_in = fsenc(tmp_opus)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",

View file

@ -678,6 +678,12 @@ except:
HAVE_BWRAP = ""
if ANYWIN:
SCWD = os.environ["systemroot"]
else:
SCWD = None
def py_desc() -> str:
interp = platform.python_implementation()
py_ver = ".".join([str(x) for x in sys.version_info])