mirror of
https://github.com/9001/copyparty.git
synced 2025-11-01 20:43:42 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d0aa20e17c | ||
|
1a658dedb7 | ||
|
8d376b854c | ||
|
490c16b01d | ||
|
2437a4e864 | ||
|
007d948cb9 | ||
|
335fcc8535 | ||
|
9eaa9904e0 | ||
|
0778da6c4d | ||
|
a1bb10012d |
@@ -84,7 +84,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [iOS shortcuts](#iOS-shortcuts) - there is no iPhone app, but
|
||||
* [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload
|
||||
* [client-side](#client-side) - when uploading files
|
||||
* [security](#security) - some notes on hardening
|
||||
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G)
|
||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||
* [cors](#cors) - cross-site request config
|
||||
* [password hashing](#password-hashing) - you can hash passwords
|
||||
@@ -1537,6 +1537,8 @@ when uploading files,
|
||||
|
||||
# security
|
||||
|
||||
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` for all important updates (at the lack of better ideas)
|
||||
|
||||
some notes on hardening
|
||||
|
||||
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.8.3"
|
||||
pkgver="1.8.6"
|
||||
pkgrel=1
|
||||
pkgdesc="Portable file sharing hub"
|
||||
arch=("any")
|
||||
@@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("6903106cab52536e5273f385813884b9c6dc734ee971ddddacfef8af6b7fec9b")
|
||||
sha256sums=("a37aacc30b9bec375ff6e7815fd763ec555b9bfbd70415aefdd18552c6491faa")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.8.3/copyparty-sfx.py",
|
||||
"version": "1.8.3",
|
||||
"hash": "sha256-jV9DUp2+lxhLP4QlIYtMoE0Woum9W4i6U/oLDyYyoRE="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.8.6/copyparty-sfx.py",
|
||||
"version": "1.8.6",
|
||||
"hash": "sha256-yTcMW4QVf1QH8jfYpn5BdG5LXilcrmakdbTk9NsVTGE="
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 8, 4)
|
||||
VERSION = (1, 8, 7)
|
||||
CODENAME = "argon"
|
||||
BUILD_DT = (2023, 7, 18)
|
||||
BUILD_DT = (2023, 7, 23)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
@@ -337,6 +337,15 @@ class HttpCli(object):
|
||||
vpath, arglist = self.req.split("?", 1)
|
||||
self.trailing_slash = vpath.endswith("/")
|
||||
vpath = undot(vpath)
|
||||
|
||||
zs = unquotep(arglist)
|
||||
m = self.conn.hsrv.ptn_cc.search(zs)
|
||||
if m:
|
||||
hit = zs[m.span()[0] :]
|
||||
t = "malicious user; Cc in query [{}] => [{!r}]"
|
||||
self.log(t.format(self.req, hit), 1)
|
||||
return False
|
||||
|
||||
for k in arglist.split("&"):
|
||||
if "=" in k:
|
||||
k, zs = k.split("=", 1)
|
||||
@@ -439,7 +448,7 @@ class HttpCli(object):
|
||||
self.can_upget,
|
||||
self.can_admin,
|
||||
) = (
|
||||
avn.can_access("", self.uname) if avn else [False] * 6
|
||||
avn.can_access("", self.uname) if avn else [False] * 7
|
||||
)
|
||||
self.avn = avn
|
||||
self.vn = vn
|
||||
@@ -488,6 +497,9 @@ class HttpCli(object):
|
||||
pex: Pebkac = ex # type: ignore
|
||||
|
||||
try:
|
||||
if pex.code == 999:
|
||||
return False
|
||||
|
||||
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
|
||||
if not self._check_nonfatal(pex, post):
|
||||
self.keepalive = False
|
||||
@@ -586,6 +598,14 @@ class HttpCli(object):
|
||||
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
|
||||
response.append("%s: %s" % (k, zs))
|
||||
|
||||
for zs in response:
|
||||
m = self.conn.hsrv.ptn_cc.search(zs)
|
||||
if m:
|
||||
hit = zs[m.span()[0] :]
|
||||
t = "malicious user; Cc in out-hdr {!r} => [{!r}]"
|
||||
self.log(t.format(zs, hit), 1)
|
||||
raise Pebkac(999)
|
||||
|
||||
try:
|
||||
# best practice to separate headers and body into different packets
|
||||
self.s.sendall("\r\n".join(response).encode("utf-8") + b"\r\n\r\n")
|
||||
@@ -784,13 +804,15 @@ class HttpCli(object):
|
||||
|
||||
path_base = os.path.join(self.E.mod, "web")
|
||||
static_path = absreal(os.path.join(path_base, self.vpath[5:]))
|
||||
if not static_path.startswith(path_base):
|
||||
t = "attempted path traversal [{}] => [{}]"
|
||||
self.log(t.format(self.vpath, static_path), 1)
|
||||
self.tx_404()
|
||||
return False
|
||||
if static_path in self.conn.hsrv.statics:
|
||||
return self.tx_file(static_path)
|
||||
|
||||
return self.tx_file(static_path)
|
||||
if not static_path.startswith(path_base):
|
||||
t = "malicious user; attempted path traversal [{}] => [{}]"
|
||||
self.log(t.format(self.vpath, static_path), 1)
|
||||
|
||||
self.tx_404()
|
||||
return False
|
||||
|
||||
if "cf_challenge" in self.uparam:
|
||||
self.reply(self.j2s("cf").encode("utf-8", "replace"))
|
||||
@@ -2985,7 +3007,11 @@ class HttpCli(object):
|
||||
if self.args.rclone_mdns or not self.args.zm
|
||||
else self.conn.hsrv.nm.map(self.ip) or host
|
||||
)
|
||||
vp = (self.uparam["hc"] or "").lstrip("/")
|
||||
# safer than html_escape/quotep since this avoids both XSS and shell-stuff
|
||||
pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "pw")
|
||||
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/")
|
||||
pw = pw.replace(" ", "%20")
|
||||
vp = vp.replace(" ", "%20")
|
||||
html = self.j2s(
|
||||
"svcs",
|
||||
args=self.args,
|
||||
@@ -2998,7 +3024,7 @@ class HttpCli(object):
|
||||
host=host,
|
||||
hport=hport,
|
||||
aname=aname,
|
||||
pw=self.pw or "pw",
|
||||
pw=pw,
|
||||
)
|
||||
self.reply(html.encode("utf-8"))
|
||||
return True
|
||||
@@ -3075,7 +3101,14 @@ class HttpCli(object):
|
||||
return True
|
||||
|
||||
def set_k304(self) -> bool:
|
||||
ck = gencookie("k304", self.uparam["k304"], self.args.R, False, 86400 * 299)
|
||||
v = self.uparam["k304"].lower()
|
||||
if v == "y":
|
||||
dur = 86400 * 299
|
||||
else:
|
||||
dur = None
|
||||
v = "x"
|
||||
|
||||
ck = gencookie("k304", v, self.args.R, False, dur)
|
||||
self.out_headerlist.append(("Set-Cookie", ck))
|
||||
self.redirect("", "?h#cc")
|
||||
return True
|
||||
@@ -3878,7 +3911,6 @@ class HttpCli(object):
|
||||
|
||||
doc = self.uparam.get("doc") if self.can_read else None
|
||||
if doc:
|
||||
doc = unquotep(doc.replace("+", " ").split("?")[0])
|
||||
j2a["docname"] = doc
|
||||
doctxt = None
|
||||
if next((x for x in files if x["name"] == doc), None):
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
||||
import base64
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
@@ -54,7 +55,6 @@ except SyntaxError:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
from .bos import bos
|
||||
from .httpconn import HttpConn
|
||||
from .u2idx import U2idx
|
||||
from .util import (
|
||||
@@ -65,6 +65,7 @@ from .util import (
|
||||
Magician,
|
||||
Netdev,
|
||||
NetMap,
|
||||
absreal,
|
||||
ipnorm,
|
||||
min_ex,
|
||||
shut_socket,
|
||||
@@ -138,6 +139,11 @@ class HttpSrv(object):
|
||||
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||
self.prism = os.path.exists(zs)
|
||||
|
||||
self.statics: set[str] = set()
|
||||
self._build_statics()
|
||||
|
||||
self.ptn_cc = re.compile(r"[\x00-\x1f]")
|
||||
|
||||
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
||||
if not self.args.no_dav:
|
||||
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
||||
@@ -168,6 +174,14 @@ class HttpSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
def _build_statics(self) -> None:
|
||||
for dp, _, df in os.walk(os.path.join(self.E.mod, "web")):
|
||||
for fn in df:
|
||||
ap = absreal(os.path.join(dp, fn))
|
||||
self.statics.add(ap)
|
||||
if ap.endswith(".gz") or ap.endswith(".br"):
|
||||
self.statics.add(ap[:-3])
|
||||
|
||||
def set_netdevs(self, netdevs: dict[str, Netdev]) -> None:
|
||||
ips = set()
|
||||
for ip, _ in self.bound:
|
||||
|
@@ -171,6 +171,7 @@ HTTPCODE = {
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
503: "Service Unavailable",
|
||||
999: "MissingNo",
|
||||
}
|
||||
|
||||
|
||||
|
@@ -2173,13 +2173,18 @@ function seek_au_sec(seek) {
|
||||
}
|
||||
|
||||
|
||||
function song_skip(n) {
|
||||
var tid = null;
|
||||
if (mp.au)
|
||||
tid = mp.au.tid;
|
||||
function song_skip(n, dirskip) {
|
||||
var tid = mp.au ? mp.au.tid : null,
|
||||
ofs = tid ? mp.order.indexOf(tid) : -1;
|
||||
|
||||
if (tid !== null)
|
||||
play(mp.order.indexOf(tid) + n);
|
||||
if (dirskip && ofs + 1 && ofs > mp.order.length - 2) {
|
||||
toast.inf(10, L.mm_nof);
|
||||
mpl.traversals = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (tid)
|
||||
play(ofs + n);
|
||||
else
|
||||
play(mp.order[n == -1 ? mp.order.length - 1 : 0]);
|
||||
}
|
||||
@@ -2194,8 +2199,9 @@ function next_song(e) {
|
||||
function next_song_cmn(e) {
|
||||
ev(e);
|
||||
if (mp.order.length) {
|
||||
var dirskip = mpl.traversals;
|
||||
mpl.traversals = 0;
|
||||
return song_skip(1);
|
||||
return song_skip(1, dirskip);
|
||||
}
|
||||
if (mpl.traversals++ < 5) {
|
||||
if (MOBILE && t_fchg && Date.now() - t_fchg > 30 * 1000)
|
||||
@@ -3930,7 +3936,7 @@ var showfile = (function () {
|
||||
if (!lang)
|
||||
continue;
|
||||
|
||||
r.files.push({ 'id': link.id, 'name': fn });
|
||||
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
|
||||
|
||||
var td = ebi(link.id).closest('tr').getElementsByTagName('td')[0];
|
||||
|
||||
@@ -4120,8 +4126,9 @@ var showfile = (function () {
|
||||
var html = ['<li class="bn">' + L.tv_lst + '<br />' + linksplit(get_vpath()).join('') + '</li>'];
|
||||
for (var a = 0; a < r.files.length; a++) {
|
||||
var file = r.files[a];
|
||||
html.push('<li><a href="?doc=' + file.name + '" hl="' + file.id +
|
||||
'">' + esc(uricom_dec(file.name)) + '</a>');
|
||||
html.push('<li><a href="?doc=' +
|
||||
uricom_enc(file.name) + '" hl="' + file.id +
|
||||
'">' + esc(file.name) + '</a>');
|
||||
}
|
||||
ebi('docul').innerHTML = html.join('\n');
|
||||
};
|
||||
|
@@ -1,3 +1,54 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0721-0036 `v1.8.6` fix reflected XSS
|
||||
|
||||
## bugfixes
|
||||
* reflected XSS through `/?hc` (the optional subfolder parameter to the [connect](https://a.ocv.me/?hc) page)
|
||||
* if someone tricked you into clicking `http://127.0.0.1:3923/?hc=<script>alert(1)</script>` they could potentially have moved/deleted existing files on the server, or uploaded new files, using your account
|
||||
* if you use a reverse proxy, you can check if you have been exploited like so:
|
||||
* nginx: grep your logs for URLs containing `?hc=` with `<` somewhere in its value, for example using the following command:
|
||||
```bash
|
||||
(gzip -dc access.log*.gz; cat access.log) | sed -r 's/" [0-9]+ .*//' | grep -E '[?&](hc|pw)=.*[<>]'
|
||||
```
|
||||
* if you find any traces of exploitation (or just want to be on the safe side) it's recommended to change the passwords of your copyparty accounts
|
||||
* thanks again to @TheHackyDog !
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0718-0746 `v1.8.4` range-select v2
|
||||
|
||||
**IMPORTANT:** `v1.8.2` (previous release) fixed [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) ; please see the [1.8.2 release notes](https://github.com/9001/copyparty/releases/tag/v1.8.2) (all serverlogs reviewed so far showed no signs of exploitation)
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)
|
||||
|
||||
## new features
|
||||
* #47 file selection by shift-clicking
|
||||
* in list-view: click a table row to select it, then shift-click another to select all files in-between
|
||||
* in grid-view: either enable the `multiselect` button (mainly for phones/tablets), or the new `sel` button in the `[⚙️] settings` tab (better for mouse+keyboard), then shift-click two files
|
||||
* volflag `fat32` avoids a bug in android's sdcardfs causing excessive reindexing on startup if any files were modified on the sdcard since last reboot
|
||||
|
||||
## bugfixes
|
||||
* minor corrections to the new features from #45
|
||||
* uploader IPs are now visible for `a`dmin accounts in `d2t` volumes as well
|
||||
|
||||
## other changes
|
||||
* the admin-panel is only accessible for accounts which have the `a` (admin) permission-level in one or more volumes; so instead of giving your user `rwmd` access, you'll want `rwmda` instead:
|
||||
```bash
|
||||
python3 copyparty-sfx.py -a joe:hunter2 -v /mnt/nas/pub:pub:rwmda,joe
|
||||
```
|
||||
or in a settings file,
|
||||
```yaml
|
||||
[/pub]
|
||||
/mnt/nas/pub
|
||||
accs:
|
||||
rwmda: joe
|
||||
```
|
||||
* until now, `rw` was enough, however most readwrite users don't need access to those features
|
||||
* grabbing a stacktrace with `?stack` is permitted for both `rw` and `a`
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0714-1558 `v1.8.2` URGENT: fix path traversal vulnerability
|
||||
|
||||
|
@@ -392,9 +392,9 @@ find -name '*.pyc' -delete
|
||||
find -name __pycache__ -delete
|
||||
find -name py.typed -delete
|
||||
|
||||
# especially prevent osx from leaking your lan ip (wtf apple)
|
||||
# especially prevent macos/osx from leaking your lan ip (wtf apple)
|
||||
find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
|
||||
find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
|
||||
find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -fv -- "$f"; done
|
||||
|
||||
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
||||
|
||||
|
@@ -69,8 +69,13 @@ def uncomment(fpath):
|
||||
def main():
|
||||
print("uncommenting", end="", flush=True)
|
||||
try:
|
||||
if sys.argv[1] == "1":
|
||||
sys.argv.remove("1")
|
||||
raise Exception("disabled")
|
||||
|
||||
import multiprocessing as mp
|
||||
|
||||
mp.set_start_method("spawn", True)
|
||||
with mp.Pool(os.cpu_count()) as pool:
|
||||
pool.map(uncomment, sys.argv[1:])
|
||||
except Exception as ex:
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
@@ -179,6 +180,8 @@ class VHttpSrv(object):
|
||||
self.gpwd = Garda("")
|
||||
self.g404 = Garda("")
|
||||
|
||||
self.ptn_cc = re.compile(r"[\x00-\x1f]")
|
||||
|
||||
def cachebuster(self):
|
||||
return "a"
|
||||
|
||||
|
Reference in New Issue
Block a user