Skip to content

Security: 修复SSRF漏洞,添加URL验证防护 - #234

Open
zjj11980 wants to merge 1 commit into
lintsinghua:v3.0.0from
zjj11980:v3.0.0
Open

Security: 修复SSRF漏洞,添加URL验证防护#234
zjj11980 wants to merge 1 commit into
lintsinghua:v3.0.0from
zjj11980:v3.0.0

Conversation

@zjj11980

Copy link
Copy Markdown

修复了 /api/v1/config/test-llm 和相关端点的SSRF(服务器端请求伪造)漏洞。 攻击者之前可以通过提供内网地址来探测内部网络服务。

主要改动:

  • 添加 is_safe_url() 函数,验证URL是否指向内网地址
  • 阻止所有内网IP范围(127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16等)
  • 阻止IPv6内网地址(::1, fc00::/7, fe80::/10)
  • 对域名进行DNS解析并检查解析后的IP地址

受保护的端点:

  • POST /api/v1/config/test-llm (测试LLM连接)
  • PUT /api/v1/config/me (更新用户配置)
  • POST /api/v1/embedding-config/test (测试嵌入模型)
  • PUT /api/v1/embedding-config/config (更新嵌入配置)

安全影响:

  • 修复前:攻击者可探测内网服务、绕过防火墙、访问内部API
  • 修复后:所有内网地址访问被阻止,返回明确错误信息

Fixes #137

修复了 /api/v1/config/test-llm 和相关端点的SSRF(服务器端请求伪造)漏洞。
攻击者之前可以通过提供内网地址来探测内部网络服务。

主要改动:
- 添加 is_safe_url() 函数,验证URL是否指向内网地址
- 阻止所有内网IP范围(127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16等)
- 阻止IPv6内网地址(::1, fc00::/7, fe80::/10)
- 对域名进行DNS解析并检查解析后的IP地址

受保护的端点:
- POST /api/v1/config/test-llm (测试LLM连接)
- PUT /api/v1/config/me (更新用户配置)
- POST /api/v1/embedding-config/test (测试嵌入模型)
- PUT /api/v1/embedding-config/config (更新嵌入配置)

安全影响:
- 修复前:攻击者可探测内网服务、绕过防火墙、访问内部API
- 修复后:所有内网地址访问被阻止,返回明确错误信息

Fixes lintsinghua#137
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Block SSRF by validating user-supplied base URLs in config and embedding endpoints

🐞 Bug fix 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add URL safety validation to block private/loopback/link-local IP SSRF targets.
• Enforce validation on LLM/embedding test and config-update endpoints before outbound calls.
• Document the SSRF issue, affected endpoints, and mitigation behavior.
Diagram

graph TD
  A["Client"] --> B["Config & Embedding APIs"] --> C{"is_safe_url?"} -->|"Yes"| D["Outbound call\nLLM/Embedding"]
  C -->|"No"| E["400 / blocked\nmessage"]
  B --> F[("UserConfig DB")]
  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _dec{"Decision"} ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize SSRF validation in a shared utility module
  • ➕ Eliminates duplicated INTERNAL_IP_RANGES/is_safe_url implementations across endpoints
  • ➕ Makes future tuning (CIDRs, schemes, DNS behavior) consistent and testable in one place
  • ➕ Encourages reuse for other URL-accepting endpoints
  • ➖ Small refactor cost and need to update imports/structure
  • ➖ Potential circular-import concerns depending on current module layout
2. Resolve all A/AAAA records and validate each (getaddrinfo)
  • ➕ Mitigates domains that resolve to multiple IPs (some internal, some public)
  • ➕ Improves IPv6 coverage vs a single IPv4 gethostbyname result
  • ➖ More code and edge cases (timeouts, multiple results)
  • ➖ May add latency if not cached or if DNS is slow
3. Adopt an allowlist approach (approved hostnames/providers only)
  • ➕ Stronger security posture than blocklists; reduces bypass risk
  • ➕ Clearer operational model for supported providers
  • ➖ Less flexible for self-hosted/custom endpoints
  • ➖ Requires product decision and configuration/UX changes

Recommendation: The current blocklist-based validation is a pragmatic fix and appropriate for a quick security patch. For maintainability and stronger SSRF resistance, move is_safe_url()/CIDR definitions into a shared security/network utility and switch DNS resolution to getaddrinfo to validate all returned A/AAAA records. Consider an allowlist model if custom base URLs are not a core requirement.

Files changed (3) +246 / -16

Bug fix (2) +176 / -16
config.pyAdd SSRF URL validation for LLM test and user config update +94/-9

Add SSRF URL validation for LLM test and user config update

• Adds INTERNAL_IP_RANGES and is_safe_url() to detect loopback/private/link-local IPv4/IPv6 targets and to DNS-resolve hostnames before allowing outbound access. Enforces validation in update_my_config (llmBaseUrl/ollamaBaseUrl) and test_llm_connection (request.baseUrl), returning/raising a clear 400-style message when blocked.

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

embedding_config.pyAdd SSRF URL validation for embedding config update and test +82/-7

Add SSRF URL validation for embedding config update and test

• Adds the same internal-range definitions and is_safe_url() logic to prevent SSRF through embedding base_url inputs. Blocks unsafe URLs in update_config (rejects persistence) and test_embedding (returns failure message before creating the embedding service).

backend/app/api/v1/endpoints/embedding_config.py

Documentation (1) +70 / -0
SSRF_FIX_SUMMARY.mdAdd SSRF remediation and endpoint coverage documentation +70/-0

Add SSRF remediation and endpoint coverage documentation

• Introduces a standalone markdown summary describing the SSRF issue, the URL validation strategy (internal IP blocking + DNS resolution), protected endpoints, and expected error messages. Serves as an audit-friendly record tied to issue #137.

SSRF_FIX_SUMMARY.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (1) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. is_safe_url() misses IPv6-mapped IPs 📎 Requirement gap ⛨ Security
Description
The new SSRF URL validation in is_safe_url() can be bypassed because it only resolves and checks a
single IPv4 address via socket.gethostbyname (missing AAAA and multi-A records) and does not block
IPv4-mapped IPv6 loopback/private addresses like ::ffff:127.0.0.1. As a result, endpoints such as
/api/v1/config/test-llm may still be able to reach internal targets despite the intended
protections.
Code

backend/app/api/v1/endpoints/config.py[R80-101]

+        # 尝试解析为IP地址
+        try:
+            ip = ipaddress.ip_address(hostname)
+            # 检查是否为内网IP
+            for network in INTERNAL_IP_RANGES:
+                if ip in network:
+                    return False
+            return True
+        except ValueError:
+            # 不是IP地址,是域名,需要解析
+            try:
+                # 解析域名获取IP地址
+                resolved_ip = socket.gethostbyname(hostname)
+                ip = ipaddress.ip_address(resolved_ip)
+                # 检查解析后的IP是否为内网IP
+                for network in INTERNAL_IP_RANGES:
+                    if ip in network:
+                        return False
+                return True
+            except (socket.gaierror, ValueError):
+                # 域名解析失败,拒绝访问
+                return False
Evidence
Compliance requires blocking internal/private/link-local targets for both IP literals and hostnames
via DNS resolution, but the current is_safe_url() implementation uses socket.gethostbyname()
which returns only one IPv4 result, while the actual outbound request is made by httpx which
performs its own DNS resolution and may select a different A record or an AAAA (IPv6) address,
creating a mismatch between validation and the real connection target. Additionally, the internal
range list and membership check (ip in network) do not account for IPv6-mapped IPv4 addresses:
ipaddress.ip_address('::ffff:127.0.0.1') is an IPv6Address, so it won’t match IPv4 networks like
127.0.0.0/8 and also isn’t covered by the listed IPv6 ranges, allowing loopback/private targets to
evade the block.

Prevent SSRF in /api/v1/config/test-llm by validating user-supplied baseUrl
backend/app/api/v1/endpoints/config.py[80-101]
backend/app/api/v1/endpoints/config.py[64-104]
backend/app/api/v1/endpoints/embedding_config.py[40-81]
backend/app/services/rag/embeddings.py[93-97]
backend/app/api/v1/endpoints/config.py[51-88]
backend/app/api/v1/endpoints/embedding_config.py[27-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`is_safe_url()` is intended to prevent SSRF by blocking connections to internal/private/link-local targets, but it can currently be bypassed because hostname validation only checks a single IPv4 address returned by `socket.gethostbyname()` (missing AAAA and multi-A records), and IP-literal validation does not correctly block IPv4-mapped IPv6 addresses (e.g., `::ffff:127.0.0.1`) that should be treated as loopback/private IPv4.

## Issue Context
- Compliance requires hostname resolution checks to prevent SSRF to localhost/private/link-local networks.
- Outbound requests are made with `httpx.AsyncClient` (or similar), which performs its own DNS resolution and may connect using any returned address family/record; therefore SSRF validation must evaluate **all** resolvable A/AAAA addresses and fail closed if any resolve to a disallowed range.
- Current logic parses the hostname/IP using `ipaddress.ip_address()` and checks membership via `ip in network` against `INTERNAL_IP_RANGES`, which does not catch IPv6-mapped IPv4 unless explicitly handled.

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[51-104]
- backend/app/api/v1/endpoints/embedding_config.py[27-81]
- backend/app/services/rag/embeddings.py[93-97]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Localhost default bypass 🐞 Bug ⛨ Security
Description
test-llm/test-embedding 只在用户显式提供 baseUrl/base_url 时校验,但当该字段为空时,下游仍会回退到 Ollama 的 localhost
默认地址并发起请求,SSRF 仍可通过选择 provider=ollama 触发。此问题使“禁止内网访问”的修复不完整。
Code

backend/app/api/v1/endpoints/config.py[R436-446]

+    # SSRF防护:验证baseUrl是否安全
+    if request.baseUrl and not is_safe_url(request.baseUrl):
+        print(f"[LLM Test] SSRF防护: 拒绝访问内网地址 {request.baseUrl}")
+        debug_info["error_type"] = "ssrf_blocked"
+        debug_info["error_category"] = "security"
+        return LLMTestResponse(
+            success=False,
+            message="不允许访问内网地址,请使用公网API地址",
+            debug=debug_info
+        )
+
Evidence
代码仅在用户提交 baseUrl/base_url 时触发 SSRF 校验,但实际网络请求的 base URL 可能来自 provider 默认值(Ollama 默认
localhost),因此在未提供 URL 的情况下仍可能访问内网/本机。

backend/app/api/v1/endpoints/config.py[373-501]
backend/app/services/llm/types.py[108-121]
backend/app/services/llm/adapters/litellm_adapter.py[151-165]
backend/app/api/v1/endpoints/embedding_config.py[384-414]
backend/app/services/rag/embeddings.py[182-206]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/api/v1/config/test-llm` 和 `/api/v1/embedding-config/test` 当前只校验请求体里“用户提供的 URL 字段”,但当该字段为空时,实际发起请求的 base URL 会由下游适配器/Provider 使用默认值(如 Ollama 的 `http://localhost:11434/...`),从而仍可触发对内网/本机地址的 SSRF。

## Issue Context
- `test_llm_connection` 在构造 `LLMConfig` 时把 `base_url` 设为 `request.baseUrl`(可能为 `None`),而 `LiteLLMAdapter` 会在 `provider==OLLAMA` 时回退到 `DEFAULT_BASE_URLS[OLLAMA]`(localhost)。
- `test_embedding` 同样在 `request.base_url` 为空时把 `None` 传入下游,`OllamaEmbedding` 会回退到 `http://localhost:11434`。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[436-501]
- backend/app/services/llm/adapters/litellm_adapter.py[151-165]
- backend/app/services/llm/types.py[108-121]
- backend/app/api/v1/endpoints/embedding_config.py[384-414]
- backend/app/services/rag/embeddings.py[182-206]

## Suggested fix
1) 在端点内对“最终将被使用的 effective base URL”做校验,而不是仅校验用户输入字段:
  - `test_llm_connection`: 先计算 `effective_base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "")`,然后对 `effective_base_url` 做 `is_safe_url` 校验;同时构造 `LLMConfig(base_url=effective_base_url)`(避免把 None 交给下游再回退)。
  - `test_embedding`: 根据 provider 推断默认 base_url(或在 EmbeddingService/Provider 层提供可查询的默认值),对 effective 值校验后再创建 service。
2) 如果业务上确实需要支持本地 Ollama:引入显式配置开关/allowlist(例如仅允许管理员、仅允许某些固定内网网段/仅允许 127.0.0.1 且不可由普通用户指定),并把默认值/前端展示与服务端校验逻辑保持一致。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Default config round-trip breaks 🐞 Bug ≡ Correctness
Description
GET 默认配置返回的 ollamaBaseUrl 是 http://localhost:11434/v1,但 PUT /api/v1/config/me
新增校验会拒绝该值为内网地址。任何客户端将默认配置原样回传(round-trip)都会收到 400,造成行为不一致。
Code

backend/app/api/v1/endpoints/config.py[R260-273]

+    # SSRF防护:验证llmBaseUrl和ollamaBaseUrl是否安全
+    if config_in.llmConfig:
+        if config_in.llmConfig.llmBaseUrl and not is_safe_url(config_in.llmConfig.llmBaseUrl):
+            print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.llmBaseUrl}")
+            raise HTTPException(
+                status_code=400,
+                detail="不允许使用内网地址作为 LLM Base URL,请使用公网API地址"
+            )
+        if config_in.llmConfig.ollamaBaseUrl and not is_safe_url(config_in.llmConfig.ollamaBaseUrl):
+            print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.ollamaBaseUrl}")
+            raise HTTPException(
+                status_code=400,
+                detail="不允许使用内网地址作为 Ollama Base URL,请使用公网API地址"
+            )
Evidence
同一服务同时“返回 localhost 作为默认值”与“拒绝保存 localhost”,从代码层面即可证明 round-trip 不一致。

backend/app/api/v1/endpoints/config.py[161-184]
backend/app/api/v1/endpoints/config.py[260-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
默认配置与更新接口的 SSRF 校验逻辑冲突:默认值包含 localhost Ollama URL,但更新接口拒绝保存内网 URL,导致 round-trip 失败。

## Issue Context
- `get_default_config()` 把 `ollamaBaseUrl` 默认设置为 `http://localhost:11434/v1`。
- `update_my_config()` 新增 SSRF 校验会拒绝 `ollamaBaseUrl` 为内网地址。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[161-184]
- backend/app/api/v1/endpoints/config.py[260-273]

## Suggested fix
选择一种一致策略并实现:
1) 如果产品目标是“严格禁止任何内网 URL”:
  - 将默认 `ollamaBaseUrl` 改为空(或移除该默认),并在前端/文档明确需要公网地址;同时在所有使用 Ollama 默认 localhost 的调用路径中禁用或改为显式配置。
2) 如果需要支持本地 Ollama:
  - 为 `ollamaBaseUrl` 引入受控 allowlist/开关(例如仅允许 127.0.0.1,或仅允许管理员/自托管模式),并让默认值与 allowlist 相匹配。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Blocking DNS in async 🐞 Bug ☼ Reliability
Description
is_safe_url() 在 async FastAPI 端点中直接调用同步 socket.gethostbyname(),会阻塞事件循环。DNS
慢或被攻击者触发大量解析时,会显著拉高延迟并降低并发处理能力。
Code

backend/app/api/v1/endpoints/config.py[R260-274]

+    # SSRF防护:验证llmBaseUrl和ollamaBaseUrl是否安全
+    if config_in.llmConfig:
+        if config_in.llmConfig.llmBaseUrl and not is_safe_url(config_in.llmConfig.llmBaseUrl):
+            print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.llmBaseUrl}")
+            raise HTTPException(
+                status_code=400,
+                detail="不允许使用内网地址作为 LLM Base URL,请使用公网API地址"
+            )
+        if config_in.llmConfig.ollamaBaseUrl and not is_safe_url(config_in.llmConfig.ollamaBaseUrl):
+            print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.ollamaBaseUrl}")
+            raise HTTPException(
+                status_code=400,
+                detail="不允许使用内网地址作为 Ollama Base URL,请使用公网API地址"
+            )
+
Evidence
新增的 SSRF 校验在多个 async 端点中调用,而实现依赖同步 socket DNS 解析;这会阻塞 event loop 并带来可靠性/性能回退。

backend/app/api/v1/endpoints/config.py[64-104]
backend/app/api/v1/endpoints/config.py[253-274]
backend/app/api/v1/endpoints/embedding_config.py[40-81]
backend/app/api/v1/endpoints/embedding_config.py[348-401]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
同步 DNS 调用(`socket.gethostbyname` / `getaddrinfo`)在 async handler 内执行会阻塞 event loop,影响整个 worker 的并发与尾延迟。

## Issue Context
当前 `update_my_config` / `test_llm_connection` / embedding 相关端点均为 `async def`,并直接调用 `is_safe_url()`,而 `is_safe_url()` 内部包含同步 DNS 解析。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[64-104]
- backend/app/api/v1/endpoints/config.py[253-274]
- backend/app/api/v1/endpoints/embedding_config.py[40-81]
- backend/app/api/v1/endpoints/embedding_config.py[348-401]

## Suggested fix
- 将 URL 校验改为 async 版本(例如 `async def is_safe_url_async(url: str) -> bool`),并使用以下任一方式避免阻塞:
 1) `await asyncio.get_running_loop().getaddrinfo(...)` 获取 DNS 结果(推荐,返回所有地址,顺带解决多记录/IPv6 覆盖)。
 2) 使用 `anyio.to_thread.run_sync(socket.getaddrinfo, ...)` / `run_in_executor` 把阻塞解析移到线程池。
- 在端点内 `await is_safe_url_async(...)`。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +80 to +101
# 尝试解析为IP地址
try:
ip = ipaddress.ip_address(hostname)
# 检查是否为内网IP
for network in INTERNAL_IP_RANGES:
if ip in network:
return False
return True
except ValueError:
# 不是IP地址,是域名,需要解析
try:
# 解析域名获取IP地址
resolved_ip = socket.gethostbyname(hostname)
ip = ipaddress.ip_address(resolved_ip)
# 检查解析后的IP是否为内网IP
for network in INTERNAL_IP_RANGES:
if ip in network:
return False
return True
except (socket.gaierror, ValueError):
# 域名解析失败,拒绝访问
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. is_safe_url() misses ipv6-mapped ips 📎 Requirement gap ⛨ Security

The new SSRF URL validation in is_safe_url() can be bypassed because it only resolves and checks a
single IPv4 address via socket.gethostbyname (missing AAAA and multi-A records) and does not block
IPv4-mapped IPv6 loopback/private addresses like ::ffff:127.0.0.1. As a result, endpoints such as
/api/v1/config/test-llm may still be able to reach internal targets despite the intended
protections.
Agent Prompt
## Issue description
`is_safe_url()` is intended to prevent SSRF by blocking connections to internal/private/link-local targets, but it can currently be bypassed because hostname validation only checks a single IPv4 address returned by `socket.gethostbyname()` (missing AAAA and multi-A records), and IP-literal validation does not correctly block IPv4-mapped IPv6 addresses (e.g., `::ffff:127.0.0.1`) that should be treated as loopback/private IPv4.

## Issue Context
- Compliance requires hostname resolution checks to prevent SSRF to localhost/private/link-local networks.
- Outbound requests are made with `httpx.AsyncClient` (or similar), which performs its own DNS resolution and may connect using any returned address family/record; therefore SSRF validation must evaluate **all** resolvable A/AAAA addresses and fail closed if any resolve to a disallowed range.
- Current logic parses the hostname/IP using `ipaddress.ip_address()` and checks membership via `ip in network` against `INTERNAL_IP_RANGES`, which does not catch IPv6-mapped IPv4 unless explicitly handled.

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[51-104]
- backend/app/api/v1/endpoints/embedding_config.py[27-81]
- backend/app/services/rag/embeddings.py[93-97]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +436 to +446
# SSRF防护:验证baseUrl是否安全
if request.baseUrl and not is_safe_url(request.baseUrl):
print(f"[LLM Test] SSRF防护: 拒绝访问内网地址 {request.baseUrl}")
debug_info["error_type"] = "ssrf_blocked"
debug_info["error_category"] = "security"
return LLMTestResponse(
success=False,
message="不允许访问内网地址,请使用公网API地址",
debug=debug_info
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Localhost default bypass 🐞 Bug ⛨ Security

test-llm/test-embedding 只在用户显式提供 baseUrl/base_url 时校验,但当该字段为空时,下游仍会回退到 Ollama 的 localhost
默认地址并发起请求,SSRF 仍可通过选择 provider=ollama 触发。此问题使“禁止内网访问”的修复不完整。
Agent Prompt
## Issue description
`/api/v1/config/test-llm` 和 `/api/v1/embedding-config/test` 当前只校验请求体里“用户提供的 URL 字段”,但当该字段为空时,实际发起请求的 base URL 会由下游适配器/Provider 使用默认值(如 Ollama 的 `http://localhost:11434/...`),从而仍可触发对内网/本机地址的 SSRF。

## Issue Context
- `test_llm_connection` 在构造 `LLMConfig` 时把 `base_url` 设为 `request.baseUrl`(可能为 `None`),而 `LiteLLMAdapter` 会在 `provider==OLLAMA` 时回退到 `DEFAULT_BASE_URLS[OLLAMA]`(localhost)。
- `test_embedding` 同样在 `request.base_url` 为空时把 `None` 传入下游,`OllamaEmbedding` 会回退到 `http://localhost:11434`。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[436-501]
- backend/app/services/llm/adapters/litellm_adapter.py[151-165]
- backend/app/services/llm/types.py[108-121]
- backend/app/api/v1/endpoints/embedding_config.py[384-414]
- backend/app/services/rag/embeddings.py[182-206]

## Suggested fix
1) 在端点内对“最终将被使用的 effective base URL”做校验,而不是仅校验用户输入字段:
   - `test_llm_connection`: 先计算 `effective_base_url = request.baseUrl or DEFAULT_BASE_URLS.get(provider, "")`,然后对 `effective_base_url` 做 `is_safe_url` 校验;同时构造 `LLMConfig(base_url=effective_base_url)`(避免把 None 交给下游再回退)。
   - `test_embedding`: 根据 provider 推断默认 base_url(或在 EmbeddingService/Provider 层提供可查询的默认值),对 effective 值校验后再创建 service。
2) 如果业务上确实需要支持本地 Ollama:引入显式配置开关/allowlist(例如仅允许管理员、仅允许某些固定内网网段/仅允许 127.0.0.1 且不可由普通用户指定),并把默认值/前端展示与服务端校验逻辑保持一致。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +260 to +274
# SSRF防护:验证llmBaseUrl和ollamaBaseUrl是否安全
if config_in.llmConfig:
if config_in.llmConfig.llmBaseUrl and not is_safe_url(config_in.llmConfig.llmBaseUrl):
print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.llmBaseUrl}")
raise HTTPException(
status_code=400,
detail="不允许使用内网地址作为 LLM Base URL,请使用公网API地址"
)
if config_in.llmConfig.ollamaBaseUrl and not is_safe_url(config_in.llmConfig.ollamaBaseUrl):
print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.ollamaBaseUrl}")
raise HTTPException(
status_code=400,
detail="不允许使用内网地址作为 Ollama Base URL,请使用公网API地址"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Blocking dns in async 🐞 Bug ☼ Reliability

is_safe_url() 在 async FastAPI 端点中直接调用同步 socket.gethostbyname(),会阻塞事件循环。DNS
慢或被攻击者触发大量解析时,会显著拉高延迟并降低并发处理能力。
Agent Prompt
## Issue description
同步 DNS 调用(`socket.gethostbyname` / `getaddrinfo`)在 async handler 内执行会阻塞 event loop,影响整个 worker 的并发与尾延迟。

## Issue Context
当前 `update_my_config` / `test_llm_connection` / embedding 相关端点均为 `async def`,并直接调用 `is_safe_url()`,而 `is_safe_url()` 内部包含同步 DNS 解析。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[64-104]
- backend/app/api/v1/endpoints/config.py[253-274]
- backend/app/api/v1/endpoints/embedding_config.py[40-81]
- backend/app/api/v1/endpoints/embedding_config.py[348-401]

## Suggested fix
- 将 URL 校验改为 async 版本(例如 `async def is_safe_url_async(url: str) -> bool`),并使用以下任一方式避免阻塞:
  1) `await asyncio.get_running_loop().getaddrinfo(...)` 获取 DNS 结果(推荐,返回所有地址,顺带解决多记录/IPv6 覆盖)。
  2) 使用 `anyio.to_thread.run_sync(socket.getaddrinfo, ...)` / `run_in_executor` 把阻塞解析移到线程池。
- 在端点内 `await is_safe_url_async(...)`。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +260 to +273
# SSRF防护:验证llmBaseUrl和ollamaBaseUrl是否安全
if config_in.llmConfig:
if config_in.llmConfig.llmBaseUrl and not is_safe_url(config_in.llmConfig.llmBaseUrl):
print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.llmBaseUrl}")
raise HTTPException(
status_code=400,
detail="不允许使用内网地址作为 LLM Base URL,请使用公网API地址"
)
if config_in.llmConfig.ollamaBaseUrl and not is_safe_url(config_in.llmConfig.ollamaBaseUrl):
print(f"[Config] SSRF防护: 拒绝保存内网地址 {config_in.llmConfig.ollamaBaseUrl}")
raise HTTPException(
status_code=400,
detail="不允许使用内网地址作为 Ollama Base URL,请使用公网API地址"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Default config round-trip breaks 🐞 Bug ≡ Correctness

GET 默认配置返回的 ollamaBaseUrl 是 http://localhost:11434/v1,但 PUT /api/v1/config/me
新增校验会拒绝该值为内网地址。任何客户端将默认配置原样回传(round-trip)都会收到 400,造成行为不一致。
Agent Prompt
## Issue description
默认配置与更新接口的 SSRF 校验逻辑冲突:默认值包含 localhost Ollama URL,但更新接口拒绝保存内网 URL,导致 round-trip 失败。

## Issue Context
- `get_default_config()` 把 `ollamaBaseUrl` 默认设置为 `http://localhost:11434/v1`。
- `update_my_config()` 新增 SSRF 校验会拒绝 `ollamaBaseUrl` 为内网地址。

## Fix Focus Areas
- backend/app/api/v1/endpoints/config.py[161-184]
- backend/app/api/v1/endpoints/config.py[260-273]

## Suggested fix
选择一种一致策略并实现:
1) 如果产品目标是“严格禁止任何内网 URL”:
   - 将默认 `ollamaBaseUrl` 改为空(或移除该默认),并在前端/文档明确需要公网地址;同时在所有使用 Ollama 默认 localhost 的调用路径中禁用或改为显式配置。
2) 如果需要支持本地 Ollama:
   - 为 `ollamaBaseUrl` 引入受控 allowlist/开关(例如仅允许 127.0.0.1,或仅允许管理员/自托管模式),并让默认值与 allowlist 相匹配。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security Issue: SSRF Vulnerability in Test LLM API Endpoint

2 participants