hiyouga/LlamaFactory · error · HTTPException

Access to private or reserved IP addresses is not allowed.

Error message

Access to private or reserved IP addresses is not allowed.

What it means

Raised as HTTP 403 by check_ssrf_url when the resolved IP address of the media URL is not is_global — i.e. it resolves to private (10/8, 192.168/16, 172.16/12), loopback, link-local, or otherwise non-global space. This is the core SSRF mitigation: the server will not fetch media from internal network addresses.

Source

Thrown at src/llamafactory/api/common.py:86


def check_ssrf_url(url: str) -> None:
    """Checks if a given URL is vulnerable to SSRF. Raises HTTPException if unsafe."""
    try:
        parsed_url = urlparse(url)
        if parsed_url.scheme not in ["http", "https"]:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only HTTP/HTTPS URLs are allowed.")

        hostname = parsed_url.hostname
        if not hostname:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid URL hostname.")

        ip_info = socket.getaddrinfo(hostname, parsed_url.port)
        ip_address_str = ip_info[0][4][0]
        ip = ipaddress.ip_address(ip_address_str)

        if not ip.is_global:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Access to private or reserved IP addresses is not allowed.",
            )

    except socket.gaierror:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail=f"Could not resolve hostname: {parsed_url.hostname}"
        )
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid URL: {e}")

View on GitHub (pinned to f28afaf635)

Solutions

  1. Serve the media from a publicly resolvable host, or expose the internal service through a public endpoint/proxy.
  2. For local media, switch to a local file path with ALLOW_LOCAL_FILES=true (goes through check_lfi_path instead).
  3. Use base64 data: URLs for small assets to avoid network fetching entirely.
  4. Note only ip_info[0] is checked — but do not rely on multi-A-record tricks; fix the addressing instead.

Example fix

// before
{type:'image_url', image_url:{url:'http://192.168.1.10:9000/bucket/img.png'}}
// after: base64 or public URL or local path
{type:'image_url', image_url:{url:'data:image/png;base64,iVBOR...'}}
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress, socket
from urllib.parse import urlparse
def url_is_public(u):
    host = urlparse(u).hostname
    try:
        ip = ipaddress.ip_address(socket.getaddrinfo(host, None)[0][4][0])
        return ip.is_global
    except (socket.gaierror, ValueError):
        return False

Type guard

const isPublicUrl = async (u) => { const h = new URL(u).hostname; const r = await dns.lookup(h); return !ipaddr.parse(r.address).rangematch('private','loopback','linkLocal'); };

Try / catch

catch (e) { if (e.status === 403 && e.detail.includes('private or reserved')) { return toDataUrl(media); } throw e; }

Prevention

When it happens

Trigger: image_url pointing at http://localhost:8000/img.png, http://192.168.1.10/cam.jpg, http://10.0.0.5/file, or an internal DNS name (e.g. http://minio.internal/img.png) that resolves to a private IP; also 169.254.169.254-style metadata endpoints.

Common situations: Running the API inside a cluster and referencing internal object storage/minio by internal name; development against localhost-hosted media; DNS rebinding-adjacent setups where a public name resolves privately.

Related errors


AI-assisted analysis of hiyouga/LlamaFactory@f28afaf635 (2026-08-14). Data as JSON: /api/errors/8ec55c7b49c30771. Report an issue: GitHub.