Skip to content

Commit 5e3c4ca

Browse files
committed
Sanitize source labels in Prometheus metrics to prevent credential leaks
Strip credentials, query parameters, and fragments from source URLs exposed as Prometheus label values. Add METRICS_INCLUDE_SOURCE_LABELS env var to optionally disable source labels entirely for sensitive network environments.
1 parent 7fa35f5 commit 5e3c4ca

File tree

3 files changed

+105
-1
lines changed

3 files changed

+105
-1
lines changed

inference/core/env.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,14 @@
271271

272272
ENABLE_PROMETHEUS = str2bool(os.getenv("ENABLE_PROMETHEUS", False))
273273

274+
# Controls whether video source URLs (e.g. RTSP endpoints) are included as
275+
# Prometheus label values. Credentials and query parameters are always
276+
# stripped, but the host/IP and path remain visible. Set to "false" to omit
277+
# source labels entirely if internal network topology is sensitive.
278+
METRICS_INCLUDE_SOURCE_LABELS = str2bool(
279+
os.getenv("METRICS_INCLUDE_SOURCE_LABELS", True)
280+
)
281+
274282
# Flag to enforce FPS, default is False
275283
ENFORCE_FPS = str2bool(os.getenv("ENFORCE_FPS", False))
276284
MAX_FPS = os.getenv("MAX_FPS")

inference/core/managers/prometheus.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import re
44
import time
55
from typing import Callable, Dict, List
6+
from urllib.parse import urlparse, urlunparse
67

78
from prometheus_client.core import REGISTRY, CounterMetricFamily, GaugeMetricFamily
89
from prometheus_client.registry import Collector
910
from prometheus_fastapi_instrumentator import Instrumentator
1011

1112
from inference.core.devices.utils import GLOBAL_INFERENCE_SERVER_ID
13+
from inference.core.env import METRICS_INCLUDE_SOURCE_LABELS
1214
from inference.core.logger import logger
1315
from inference.core.managers.metrics import get_model_metrics
1416

@@ -125,13 +127,26 @@ def _average_source_fps(sources_metadata: List[dict]) -> float:
125127
return 0.0
126128
return sum(values) / len(values)
127129

130+
@staticmethod
131+
def _sanitize_source_reference(ref: str) -> str:
132+
"""Strip credentials and query parameters from URLs to avoid leaking
133+
secrets in metrics."""
134+
parsed = urlparse(ref)
135+
if parsed.scheme and parsed.hostname:
136+
netloc = parsed.hostname + (f":{parsed.port}" if parsed.port else "")
137+
sanitized = parsed._replace(netloc=netloc, query="", fragment="")
138+
return urlunparse(sanitized)
139+
return ref
140+
128141
@staticmethod
129142
def _extract_source_label(sources_metadata: List[dict]) -> str:
143+
if not METRICS_INCLUDE_SOURCE_LABELS:
144+
return ""
130145
refs = []
131146
for src in sources_metadata:
132147
ref = src.get("source_reference")
133148
if ref is not None:
134-
refs.append(str(ref))
149+
refs.append(CustomCollector._sanitize_source_reference(str(ref)))
135150
return ",".join(refs) if refs else ""
136151

137152
def sanitize_string(self, input_string):

tests/inference/unit_tests/core/managers/test_prometheus.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,62 @@ def test_negative_fps_excluded(self):
307307
assert CustomCollector._average_source_fps(metadata) == 25.0
308308

309309

310+
class TestSanitizeSourceReference:
311+
def test_strips_credentials_from_rtsp_url(self):
312+
assert (
313+
CustomCollector._sanitize_source_reference("rtsp://admin:secret@192.168.1.1:554/stream1")
314+
== "rtsp://192.168.1.1:554/stream1"
315+
)
316+
317+
def test_strips_credentials_from_http_url(self):
318+
assert (
319+
CustomCollector._sanitize_source_reference("http://user:pass@example.com:8080/feed")
320+
== "http://example.com:8080/feed"
321+
)
322+
323+
def test_strips_username_only(self):
324+
assert (
325+
CustomCollector._sanitize_source_reference("rtsp://admin@10.0.0.1/live")
326+
== "rtsp://10.0.0.1/live"
327+
)
328+
329+
def test_preserves_url_without_credentials(self):
330+
assert (
331+
CustomCollector._sanitize_source_reference("rtsp://192.168.1.1:554/stream")
332+
== "rtsp://192.168.1.1:554/stream"
333+
)
334+
335+
def test_preserves_device_index(self):
336+
assert CustomCollector._sanitize_source_reference("0") == "0"
337+
338+
def test_preserves_file_path(self):
339+
assert CustomCollector._sanitize_source_reference("/dev/video0") == "/dev/video0"
340+
341+
def test_preserves_regular_file_path(self):
342+
assert (
343+
CustomCollector._sanitize_source_reference("/home/user/video.mp4")
344+
== "/home/user/video.mp4"
345+
)
346+
347+
def test_strips_credentials_and_query_params(self):
348+
assert (
349+
CustomCollector._sanitize_source_reference("rtsp://user:p%40ss@cam.local:554/ch1?transport=tcp")
350+
== "rtsp://cam.local:554/ch1"
351+
)
352+
353+
def test_strips_query_params_without_credentials(self):
354+
assert (
355+
CustomCollector._sanitize_source_reference("rtsp://cam.local:554/stream?token=secret123&channel=1")
356+
== "rtsp://cam.local:554/stream"
357+
)
358+
359+
def test_strips_fragment(self):
360+
assert (
361+
CustomCollector._sanitize_source_reference("http://example.com/feed#section")
362+
== "http://example.com/feed"
363+
)
364+
365+
310366
class TestExtractSourceLabel:
311367
def test_single_source(self):
312368
metadata = [
@@ -338,6 +394,31 @@ def test_none_source_reference_excluded(self):
338394
]
339395
assert CustomCollector._extract_source_label(metadata) == "rtsp://cam1.local/stream"
340396

397+
def test_strips_credentials_from_source_references(self):
398+
metadata = [
399+
{"source_reference": "rtsp://admin:secret@192.168.1.1:554/stream", "source_id": 0},
400+
{"source_reference": "rtsp://user:pass@10.0.0.1:554/live", "source_id": 1},
401+
]
402+
assert (
403+
CustomCollector._extract_source_label(metadata)
404+
== "rtsp://192.168.1.1:554/stream,rtsp://10.0.0.1:554/live"
405+
)
406+
407+
def test_strips_query_params_from_source_references(self):
408+
metadata = [
409+
{"source_reference": "rtsp://cam.local/stream?token=secret&channel=1", "source_id": 0},
410+
]
411+
assert CustomCollector._extract_source_label(metadata) == "rtsp://cam.local/stream"
412+
413+
def test_returns_empty_when_source_labels_disabled(self):
414+
metadata = [
415+
{"source_reference": "rtsp://cam1.local/stream", "source_id": 0},
416+
]
417+
with patch(
418+
"inference.core.managers.prometheus.METRICS_INCLUDE_SOURCE_LABELS", False
419+
):
420+
assert CustomCollector._extract_source_label(metadata) == ""
421+
341422

342423
def _find_samples(metric_families, name):
343424
"""Return all samples for a given metric family name."""

0 commit comments

Comments
 (0)