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
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-roothave it read files under that root with your account's rights.triton/README.mdsays 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;/docsand/redocload 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.