Review

Probe carrier logger URLs before cold-chain onboarding

ColdPack Trace onboards 3PL temperature-logger APIs by fetching a sample /health payload from the carrier-supplied HTTPS URL. Blocks private/loopback/link-local resolved addresses so partner probes cannot reach cluster metadata or internal APIs. Returns logger firmware/build so ops can approve the feed before cron pulls start.

FastAPITier 6securityssrfdnshttpx

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

services/carriers/logger_probe.py+32-6
1+import ipaddress
2+import socket
13from urllib.parse import urlparse
24
35import httpx
46from fastapi import APIRouter, HTTPException
57from pydantic import BaseModel, HttpUrl
68
79router = APIRouter(prefix="/carriers", tags=["carriers"])
810
11+
12+def _resolves_only_public(hostname: str) -> bool:
13+ """Reject hosts whose current A/AAAA records are non-public."""
14+ for family, _, _, _, sockaddr in socket.getaddrinfo(hostname, None):
15+ ip = ipaddress.ip_address(sockaddr[0])
16+ if (
17+ ip.is_private
18+ or ip.is_loopback
19+ or ip.is_link_local
20+ or ip.is_reserved
21+ or ip.is_multicast
22+ ):
23+ return False
24+ return True
25+
9-@router.post("/{carrier_id}/probe-logger")
10-async def probe_logger(carrier_id: str, body: ProbeBody):
11- # TODO: block internal IPs before fetching partner logger health
12- async with httpx.AsyncClient(timeout=5.0) as client:
13- resp = await client.get(str(body.endpoint_url))
14- return {"carrier_id": carrier_id, "status": resp.status_code, "body": resp.text[:512]}
26+@router.post("/{carrier_id}/probe-logger")
27+async def probe_logger(carrier_id: str, body: ProbeBody):
28+ parsed = urlparse(str(body.endpoint_url))
29+ if parsed.scheme != "https" or not parsed.hostname:
30+ raise HTTPException(status_code=400, detail="https endpoints only")
31+ if not _resolves_only_public(parsed.hostname):
32+ raise HTTPException(status_code=400, detail="endpoint resolves to a non-public address")
33+ # Safe: we already verified the resolved address above
34+ async with httpx.AsyncClient(timeout=5.0, follow_redirects=False) as client:
35+ resp = await client.get(str(body.endpoint_url))
36+ return {
37+ "carrier_id": carrier_id,
38+ "status": resp.status_code,
39+ "body": resp.text[:512],
40+ }