Skip to content

Security Issue: SSRF Vulnerability in Test LLM API Endpoint #137

Description

@flashzyc

Security Issue: SSRF Vulnerability in Test LLM API Endpoint

Summary

A Server-Side Request Forgery (SSRF) vulnerability has been identified in the /api/v1/config/test-llm endpoint. This vulnerability allows attackers to probe internal network services by observing different response times and behaviors when accessing open versus closed ports on internal hosts.

Vulnerability Details

Affected Endpoint: /api/v1/config/test-llm

Severity: High

Type: Server-Side Request Forgery (SSRF)

Description

The endpoint accepts user-controlled URLs for LLM API testing without proper validation. When the endpoint attempts to connect to internal network addresses, it exhibits different response characteristics depending on whether the target port is open or closed. This behavior difference can be exploited to:

  1. Map internal network topology
  2. Identify running services on internal hosts
  3. Potentially access sensitive internal services
  4. Bypass firewall restrictions

Note: Although this is a backend authenticated endpoint, the application contains hardcoded demo credentials that allow anyone to access the admin panel:

  • Email: demo@example.com
  • Password: demo123

These credentials are documented in the deployment guide and FAQ, making it trivial for attackers to authenticate and exploit this SSRF vulnerability without any real access barriers.

Vulnerable Code

File: backend/app/api/v1/endpoints/config.py

@router.post("/test-llm", response_model=LLMTestResponse)
async def test_llm_connection(
    request: LLMTestRequest,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(deps.get_current_user),
) -> Any:
    """测试LLM连接是否正常"""
    # ... code ...
  
    # Issue: baseUrl accepts user input without validation
    base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "")
  
    # ... code ...
  
    # Creates config with user-controlled URL
    config = LLMConfig(
        provider=provider,
        api_key=request.apiKey,
        model=model,
        base_url=request.baseUrl,  # ⚠️ No validation for internal IPs
        timeout=test_timeout,
        temperature=test_temperature,
        max_tokens=test_max_tokens,
    )
  
    # Adapter makes request to user-controlled URL
    adapter = LiteLLMAdapter(config)
    response = await adapter.complete(test_request)

The request.baseUrl parameter is directly used without checking if it points to internal network addresses, allowing SSRF attacks.

Proof of Concept

Request Packet

POST /api/v1/config/test-llm HTTP/1.1
Host: localhost:3000
sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"
Origin: http://localhost:3000
Sec-Fetch-Dest: empty
sec-ch-ua-platform: "Windows"
Cookie: ECS[visit_times]=4
Sec-Fetch-Mode: cors
Referer: http://localhost:3000/admin
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Njk1NjE2MjYsInN1YiI6ImQ4NGQ3YTUwLTZjZDEtNDQ3MC05YWQwLWZlZmM0Zjg2MDJkYSJ9.isM5DVTUWQwf4xLEqyNEURYwEqo3g_lrPqDYzCuSId8
sec-ch-ua-mobile: ?0
Accept: application/json, text/plain, */*
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Content-Length: 157

{"provider":"openai","apiKey":"sk-Qx9F2aL7MZpR3K4HnC8WJd0VtUeS1YB6GmE5rAqPSoLkX","model":"gemini-3-flash-preview","baseUrl":"http://127.0.0.1:8001"}

Response Time Comparison

Case 1: Internal service exists (port open)

Image

Case 2: Internal service does not exist (port closed)

Image

Observable Differences

  • Response Time: Significant difference in response latency between open and closed ports
  • Status Codes: Different HTTP status codes returned
  • Error Messages: Varying error messages that leak internal network information

Impact

This vulnerability can be exploited to:

  • Scan internal network infrastructure
  • Identify internal services and their availability
  • Potentially access or interact with internal APIs, databases, or services
  • Bypass network segmentation and access controls
  • Perform denial of service attacks against internal services

Exploitation Barrier: Low - The hardcoded demo credentials (demo@example.com / demo123) effectively make this an unauthenticated SSRF vulnerability, as any attacker can easily obtain valid authentication tokens.

Affected Versions

  • All versions prior to fix

Recommended Fix

Add URL validation to block internal network addresses before making requests.

Implementation:

import ipaddress
import socket
from urllib.parse import urlparse

INTERNAL_IP_RANGES = [
    ipaddress.ip_network('127.0.0.0/8'),      # Loopback
    ipaddress.ip_network('10.0.0.0/8'),       # Private Class A
    ipaddress.ip_network('172.16.0.0/12'),    # Private Class B
    ipaddress.ip_network('192.168.0.0/16'),   # Private Class C
    ipaddress.ip_network('169.254.0.0/16'),   # Link-local
]

def is_safe_url(url: str) -> bool:
    """Validate URL to prevent SSRF attacks"""
    if not url:
        return True
  
    parsed = urlparse(url)
    hostname = parsed.hostname
  
    if not hostname:
        return True
  
    # Block localhost
    if hostname in ['localhost', '127.0.0.1', '0.0.0.0', '::1']:
        return False
  
    # Resolve and check IP address
    try:
        ip = ipaddress.ip_address(hostname)
        # Direct IP check
        return not any(ip in network for network in INTERNAL_IP_RANGES)
    except ValueError:
        # It's a hostname, resolve it
        try:
            resolved_ip = socket.gethostbyname(hostname)
            ip = ipaddress.ip_address(resolved_ip)
            return not any(ip in network for network in INTERNAL_IP_RANGES)
        except (socket.gaierror, ValueError):
            return False

# Add validation in the endpoint:
@router.post("/test-llm", response_model=LLMTestResponse)
async def test_llm_connection(request: LLMTestRequest, ...):
    # Validate baseUrl before using it
    if request.baseUrl and not is_safe_url(request.baseUrl):
        raise HTTPException(
            status_code=400,
            detail="Access to internal network addresses is not allowed"
        )
  
    # ... rest of the code ...

Reporter: flashzyc
Date: January 20, 2026

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions