diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ec75dfc6..ac99f24d 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -827,7 +827,8 @@ def add_network(ap): ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 even if the NIC has routable IPs (breaks some mdns clients)") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy") - ap2.add_argument("--ip-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from (argument must be lowercase, but not the actual header)") + ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from (argument must be lowercase, but not the actual header)") + ap2.add_argument("--xff-src", metavar="IP", type=u, default="127., ::1", help="comma-separated list of trusted reverse-proxy IPs; only accept the real-ip header (--xff-hdr) if the incoming connection is from an IP starting with either of these. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using --xff-hdr=cf-connecting-ip (or similar)") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here (eg. /foo/bar)") if ANYWIN: ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") @@ -1017,8 +1018,8 @@ def add_safety(ap): ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (422 is server fuzzing, invalid POSTs and so)") ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (decent replacement for --ban-404 if that can't be used)") - ap2.add_argument("--sus-urls", metavar="REGEX", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") - ap2.add_argument("--nonsus-urls", metavar="REGEX", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") + ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") + ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0") 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 B 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") @@ -1049,7 +1050,7 @@ def add_logging(ap): ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") - ap2.add_argument("--log-tdec", type=int, default=3, help="timestamp resolution / number of timestamp decimals") + ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 80f5999c..c247f50f 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -283,22 +283,35 @@ class HttpCli(object): n = self.args.rproxy if n: - zso = self.headers.get(self.args.ip_hdr) - if zso and self.conn.addr[0] in ["127.0.0.1", "::1"]: + zso = self.headers.get(self.args.xff_hdr) + if zso: if n > 0: n -= 1 zsl = zso.split(",") try: - self.ip = zsl[n].strip() + cli_ip = zsl[n].strip() except: - self.ip = zsl[0].strip() + cli_ip = zsl[0].strip() t = "rproxy={} oob x-fwd {}" self.log(t.format(self.args.rproxy, zso), c=3) - self.log_src = self.conn.set_rproxy(self.ip) - self.is_vproxied = bool(self.args.R) - self.host = self.headers.get("x-forwarded-host") or self.host + pip = self.conn.addr[0] + if self.args.xff_re and not self.args.xff_re.match(pip): + t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"' + if self.headers.get("cf-connecting-ip"): + t += " Alternatively, if you are behind cloudflare, it is better to specify these two instead: --xff-hdr=cf-connecting-ip --xff-src=any" + zs = ( + ".".join(pip.split(".")[:2]) + "." + if "." in pip + else ":".join(pip.split(":")[:4]) + ":" + ) + self.log(t % (self.args.xff_hdr, pip, cli_ip, zso, zs), 3) + else: + self.ip = cli_ip + self.is_vproxied = bool(self.args.R) + self.log_src = self.conn.set_rproxy(self.ip) + self.host = self.headers.get("x-forwarded-host") or self.host if self.is_banned(): return False diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 74323898..1518b5c3 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -420,6 +420,12 @@ class SvcHub(object): elif al.ban_url == "no": al.sus_urls = None + if al.xff_src in ("any", "0", ""): + al.xff_re = None + else: + zs = al.xff_src.replace(" ", "").replace(".", "\\.").replace(",", "|") + al.xff_re = re.compile("^(?:" + zs + ")") + return True def _setlimits(self) -> None: