|
Download src/docs/SECURITY.md from CIIL/Anuvani-Diarization: direct link, hf CLI and curl.
- Browser
- Download file 7.5 kB
-
https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/docs/SECURITY.md
- Command line
-
hf download hf://CIIL/Anuvani-Diarization/src/docs/SECURITY.md
-
curl -L -o SECURITY.md https://huggingface.co/CIIL/Anuvani-Diarization/resolve/main/src/docs/SECURITY.md
7.5 kB
| # Security review of the serving code | |
| **When and how.** 8 October 2026, a read-only review of the gateway: trace every untrusted input to every sink, | |
| confirm each candidate end to end, keep only findings with confidence 8 of 10 or more. | |
| **Scope.** `crates/diar-gateway`, `crates/diar-triton`, `crates/diar-audio` (decoding untrusted uploads), the | |
| engine's input path, `python/anuvani_diar/client.py`, the Triton models, image and scripts, and the compose stack. | |
| Threat model: clients are untrusted; the gateway binds 127.0.0.1 by default but may be exposed; Triton listens on | |
| 127.0.0.1 only; the host may have other local accounts. | |
| ## Findings and fixes | |
| | # | Finding | Severity | Fix | Test | | |
| |---|---|---|---|---| | |
| | 1 | The Python client sent its bearer token along any HTTP redirect (urllib copies every request header to the redirect target), so a forged 302 on an exposed, non-TLS deployment would hand the token to another host | medium | the client never follows redirects; a 3xx is an error (the gateway never redirects) | `test_client_does_not_follow_redirects` | | |
| | 2 | The size limit and the memory budget were checked on the decoded audio only; resampling to 16 kHz came after, unchecked, so a file declaring a low sample rate (libsndfile accepts any) could grow up to 16,000 times past both, e.g. 2.4 MB at 1 Hz becoming 38.4 GB | medium | the resampled size, `ceil(frames x 16000 / rate)` samples, is computed from the header with the decoded one; both must be within `--max-audio-gb`, and the budget holds both | `test_a_low_sample_rate_cannot_resample_past_the_limit` (engine and gateway) | | |
| | 3 | Grafana allowed anonymous viewing; on a host with other accounts the loopback is theirs too | low | anonymous access off; the admin password is generated into `artifacts/compose/grafana_password` (mode 0600) | configuration | | |
| Checked and found sound (from the review): the token middleware covers every route but `/health`, with a | |
| constant-time comparison; upload names, uris and spool files (no path separators, exclusive creation with mode | |
| 0600 in a directory refused unless it is ours and 0700); path jobs (canonical path under the allowed root, regular | |
| files only, one generic refusal message); no injection into RTTM lines or JSON through `uri`; Triton responses | |
| parsed with checked slicing only; the `unsafe` blocks in decoding and tensor encoding; panics contained per file | |
| (engine) or per request (axum); no token or server path in logs or error messages; every published port on | |
| 127.0.0.1, read-only mounts, no privileged containers. | |
| ## What remains (by design or out of scope) | |
| - **libsndfile parses untrusted uploads inside the gateway process**, as soundfile does inside the release. A | |
| parser bug in libsndfile 1.2.2 would take the gateway down. 2,750 damaged files decode exactly as soundfile does | |
| (`tests/test_audio_decode.py`), which is a check of equivalence, not of libsndfile's own safety. Decoding in a | |
| separate process would contain such a bug; not done. | |
| - **Without a token, any local account can use the gateway**, and with `--allow-path-root` have it read files under | |
| that root with your account's rights. `triton/README.md` says to set a token on a shared machine. | |
| - **Path jobs**: the file is opened by its canonical path after the check, so someone who can write inside the | |
| allowed root could swap a path component in between. That needs write access inside a root the operator chose. | |
| - **Jobs live in the gateway's memory**: a gateway restart loses them; the Python client resubmits (tested), other | |
| clients must. | |
| ## Second review: the FastAPI service (9 October 2026) | |
| A read-only review of `triton_inference/service` (the FastAPI app, its settings, spool, Dockerfile and build script) | |
| and its Rust binding (`crates/diar-py/src/service.rs`), against the pinned libraries (fastapi 0.143.0, starlette | |
| 1.7.0, uvicorn 0.54.0, h11 0.16.0, python-multipart 0.0.32). Two findings were confirmed by offline measurement | |
| before they were fixed. | |
| | # | Finding | Severity | Fix | Test | | |
| |---|---|---|---|---| | |
| | 4 | The resampling filter was not counted. torchaudio's sinc filter for a rate `sr` is a (16000 / g, 2 x width + sr / g) table of float64, g = gcd(sr, 16000), whatever the file's length; a 500-byte WAV declaring 16,001 Hz asks for 2 GB per table (measured: +4.2 GB peak, 4.4 s), one declaring 1,000,003 Hz for about 128 GB. Item 2's limits saw only 400 bytes decoded and 8 bytes resampled. This reached the gateway and the pip package too (same engine) | high | the engine computes the filter's size from the header's rate (`resample_filter_bytes`, checked against torchaudio 2.1.1's own tables) and refuses a file whose filter exceeds 64 MiB before anything is resampled; every common rate (8 to 192 kHz) needs under 3 MB. A file the release would have resampled at such a rate is now refused: the one place where the engine's behaviour departs from the release's, for headers no recorder writes | `test_a_rate_that_does_not_reduce_cannot_build_a_huge_resampling_filter` (engine and service); `resample_filter_sizes` (Rust) | | |
| | 5 | Multipart bodies were bounded only in their file part: parts with no data but distinct names of up to 4 KiB each were kept in a dict without limit, and with `Transfer-Encoding: chunked` (no `Content-Length`) nothing capped the non-file bytes (measured: memory grew 1:1 with the bytes sent) | high | at most 16 parts; boundaries, part headers, fields and anything after the closing boundary together within 128 KiB above `--max-upload-mb`, checked while streaming | `test_multipart_framing_is_bounded` | | |
| | 6 | The image's default command listened on 0.0.0.0 and a token was optional, so `docker run --network host` without a token exposed the API on every interface | medium | `--listen` on an address other than the loopback refuses to start without a token, unless `--insecure-no-token` | `test_listening_beyond_the_loopback_needs_a_token` | | |
| | 7 | An unusual spool `OSError` without `strerror` put the spool file's path into the error message | low | a generic message instead | review | | |
| Not changed, recorded: | |
| - **One token, one trust domain.** Any holder of the token can read or cancel any job and replay any | |
| `Idempotency-Key`. Job ids are 128 random bits, and the result cache answers only for audio the caller sent, so | |
| nothing leaks between callers who know nothing of each other's jobs; but the service is single-tenant. Several | |
| tenants need a reverse proxy with per-tenant tokens in front, or per-token scoping (not done). | |
| - **Path jobs** keep the gateway's check-then-open gap (above). | |
| - **Base images** are pinned by tag (`ubuntu:22.04`, `tritonserver:24.10-py3`, `tritonserver:25.06-py3`), not by digest; `/docs` and `/redoc` | |
| load their JavaScript from cdn.jsdelivr.net (FastAPI's defaults), behind the token. | |
| ## Tritons on other machines (10 October 2026) | |
| `diar up --tritons-only --bind ADDR` publishes a machine's Tritons on ADDR so that another machine's service can use | |
| its GPUs (`diar up --remote`). Triton has no authentication: anyone who reaches those ports can run the networks and | |
| read the server's metadata. The Tritons start with `--model-control-mode=none`, so models can be neither loaded nor | |
| unloaded through the API, but the ports must stay on a private network: bind to a private address (the command | |
| refuses to publish without `--bind`), never to a public interface, and firewall the ports to the service's machine. | |
| The service's own token and limits are unchanged. | |