Three unbounded-input problems in the worker/hub tier:
-
Worker-side path traversal via job_id. worker.py:100 builds adapter = self._work_root / (msg.get("job_id") or "") from a hub-controlled websocket frame with no validation, then passes it to load(msg["base_model"], adapter=str(adapter)) at worker.py:110. A job_id of ../../.. escapes the work root. Workers trust the hub by design, but the id should still be validated as a single safe path segment (no separators, no ..).
-
No cap on websocket frame size. ws.py:63-73 recv_frame reads a 64-bit payload length and allocates it via _read_exact with no maximum. Either side of the hub↔worker socket can trigger unbounded memory allocation with one frame header. A configurable cap (even a generous 512 MB default for adapter tarballs) turns this into a clean protocol error.
-
No cap on HTTP request bodies. serve.py:1191 _body() reads Content-Length uncapped, and the artifact upload at serve.py:1426 does self.rfile.read(length) straight into memory. Same fix: reject bodies over a limit with a 413.
Related hardening in the same area, lower severity:
- Path checks on static assets are
".." substring denylists (serve.py:1216, serve.py:1226) and dataset ids flow raw into self.root / f"{ds_id}.json" (serve.py:1250, serve.py:333). Safe today only because BaseHTTPRequestHandler does not URL-decode self.path. Resolve-and-contain (Path.resolve().is_relative_to(root)) is the robust form.
POST /v1/login (serve.py:1343) has no rate limit or lockout, so unlimited password guessing is possible when auth is enabled.
- No token revocation:
_valid_token (serve.py:1141) checks only expiry and signature, so a leaked 12h token cannot be invalidated except by changing the password.
- The HF token is written world-readable:
serve.py:477 writes settings.json with the default umask, and there is no chmod/0o600 anywhere in the package. Same for tokens.json (serve.py:495). Both should be written 0600.
Three unbounded-input problems in the worker/hub tier:
Worker-side path traversal via
job_id.worker.py:100buildsadapter = self._work_root / (msg.get("job_id") or "")from a hub-controlled websocket frame with no validation, then passes it toload(msg["base_model"], adapter=str(adapter))atworker.py:110. Ajob_idof../../..escapes the work root. Workers trust the hub by design, but the id should still be validated as a single safe path segment (no separators, no..).No cap on websocket frame size.
ws.py:63-73 recv_framereads a 64-bit payload length and allocates it via_read_exactwith no maximum. Either side of the hub↔worker socket can trigger unbounded memory allocation with one frame header. A configurable cap (even a generous 512 MB default for adapter tarballs) turns this into a clean protocol error.No cap on HTTP request bodies.
serve.py:1191 _body()readsContent-Lengthuncapped, and the artifact upload atserve.py:1426doesself.rfile.read(length)straight into memory. Same fix: reject bodies over a limit with a 413.Related hardening in the same area, lower severity:
".."substring denylists (serve.py:1216,serve.py:1226) and dataset ids flow raw intoself.root / f"{ds_id}.json"(serve.py:1250,serve.py:333). Safe today only becauseBaseHTTPRequestHandlerdoes not URL-decodeself.path. Resolve-and-contain (Path.resolve().is_relative_to(root)) is the robust form.POST /v1/login(serve.py:1343) has no rate limit or lockout, so unlimited password guessing is possible when auth is enabled._valid_token(serve.py:1141) checks only expiry and signature, so a leaked 12h token cannot be invalidated except by changing the password.serve.py:477writessettings.jsonwith the default umask, and there is nochmod/0o600anywhere in the package. Same fortokens.json(serve.py:495). Both should be written 0600.