Anuvani-Diarization / src /docs /SECURITY.md
StephennFernandes's picture
Anuvani-Diarization 1.0.0
63dd946
|
Raw History Blame Contribute Delete
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.