🐛 fix: distinguish SSRF block errors from network errors (#13103)

This commit is contained in:
YuTengjing
2026-03-18 18:16:19 +08:00
committed by GitHub
parent 8a90f79c11
commit 1df5ae32f1
7 changed files with 96 additions and 92 deletions

View File

@@ -19,9 +19,10 @@ LOBE_PORT=3210
RUSTFS_PORT=9000
RUSTFS_ADMIN_PORT=9001
APP_URL=http://localhost:3210
# INTERNAL_APP_URL is optional, used for server-to-server calls
# to bypass CDN/proxy. If not set, defaults to APP_URL.
# Example: INTERNAL_APP_URL=http://localhost:3210
# INTERNAL_APP_URL is used for internal server-to-server communication.
# Required for Docker Compose deployments, otherwise features like
# AI image generation will fail when APP_URL is a host/LAN IP.
INTERNAL_APP_URL=http://localhost:3210
# Secrets (auto-generated by setup.sh)
KEY_VAULTS_SECRET=YOUR_KEY_VAULTS_SECRET

View File

@@ -17,9 +17,9 @@
# 如没有特殊需要不用更改
LOBE_PORT=3210
APP_URL=http://localhost:3210
# 内部应用URL是可选的,用于服务器内部调用
# 如果没有设置,默认使用 APP_URL
# INTERNAL_APP_URL=http://localhost:3210
# 内部应用URL用于器内部服务间通信
# Docker Compose 部署时必须配置,否则当 APP_URL 为宿主机 IP 时 AI 生图等功能会失败
INTERNAL_APP_URL=http://localhost:3210
# 密钥配置(由 setup.sh 自动生成)
KEY_VAULTS_SECRET=YOUR_KEY_VAULTS_SECRET

View File

@@ -18,6 +18,7 @@ services:
- 'KEY_VAULTS_SECRET=${KEY_VAULTS_SECRET}'
- 'AUTH_SECRET=${AUTH_SECRET}'
- 'DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgresql:5432/${LOBE_DB_NAME}'
- 'INTERNAL_APP_URL=http://localhost:3210'
- 'S3_ENDPOINT=${S3_ENDPOINT}'
- 'S3_BUCKET=${RUSTFS_LOBE_BUCKET}'
- 'S3_ENABLE_PATH_STYLE=1'

View File

@@ -308,6 +308,23 @@ services:
- 'DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgresql:5432/${LOBE_DB_NAME}'
```
3. Internal Communication URL (`INTERNAL_APP_URL`)
<Callout type="warning">
`INTERNAL_APP_URL` is **required** for Docker Compose deployments. LobeHub's async features (e.g., AI image generation) require the container to make HTTP requests to itself internally. If `INTERNAL_APP_URL` is not set, the system falls back to `APP_URL`. When `APP_URL` is a host IP (e.g., `http://10.1.7.146:8080`), the container cannot reach that address internally, causing async features like image generation to silently fail.
</Callout>
```env
INTERNAL_APP_URL=http://localhost:3210
```
- `APP_URL`: Used for browser/client access, OAuth callbacks, webhooks, etc.
- `INTERNAL_APP_URL`: Used for internal server-to-server communication within the container (bypasses CDN/proxy/host network)
<Callout type="tip">
For Docker Compose deployments, we recommend using `http://localhost:3210` (container calling itself) or `http://lobe:3210` (using service name for container-to-container communication).
</Callout>
## FAQ
#### Database Migration Issues
@@ -330,37 +347,6 @@ sudo rm -rf ./data # Remove mounted database data
docker compose up -d # Restart
```
#### Using `INTERNAL_APP_URL` for Internal Server Communication
<Callout type="info">
If you're deploying LobeHub behind a CDN (like Cloudflare) or reverse proxy, you may want to configure internal server-to-server communication to bypass the CDN/proxy layer for better performance.
</Callout>
You can configure the `INTERNAL_APP_URL` environment variable:
```yaml
environment:
- 'APP_URL=https://lobe.example.com' # Public URL for browser access
- 'INTERNAL_APP_URL=http://localhost:3210' # Internal URL for server-to-server calls
```
**How it works:**
- `APP_URL`: Used for browser/client access, OAuth callbacks, webhooks, etc. (goes through CDN/proxy)
- `INTERNAL_APP_URL`: Used for internal server-to-server communication (bypasses CDN/proxy)
If `INTERNAL_APP_URL` is not set, it defaults to `APP_URL`.
**Configuration options:**
- `http://localhost:3210` - If using Docker with host network mode
- `http://lobe:3210` - If using Docker network with service name
- `http://127.0.0.1:3210` - Alternative localhost address
<Callout type="tip">
For Docker Compose deployments, we recommend using `http://lobe:3210` as the `INTERNAL_APP_URL` (using service name for container-to-container communication).
</Callout>
## Reverse Proxy Configuration
For production deployments with a custom domain and HTTPS, configure a reverse proxy in front of LobeHub.

View File

@@ -302,6 +302,23 @@ services:
- 'DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgresql:5432/${LOBE_DB_NAME}'
```
3. 内部通信地址 (`INTERNAL_APP_URL`)
<Callout type="warning">
Docker Compose 部署时**必须配置** `INTERNAL_APP_URL`。LobeHub 的异步功能(如 AI 生图)需要容器内部发起 HTTP 请求调用自身。如果未设置 `INTERNAL_APP_URL`,系统将使用 `APP_URL` 作为回退。当 `APP_URL` 是宿主机 IP如 `http://10.1.7.146:8080`)时,容器内部无法访问该地址,导致生图等异步功能静默失败。
</Callout>
```env
INTERNAL_APP_URL=http://localhost:3210
```
- `APP_URL`:用于浏览器 / 客户端访问、OAuth 回调、webhook 等
- `INTERNAL_APP_URL`:用于容器内部服务间通信(绕过 CDN / 代理 / 宿主机网络)
<Callout type="tip">
对于 Docker Compose 部署,推荐使用 `http://localhost:3210`(容器调用自身)或 `http://lobe:3210`(使用服务名称进行容器间通信)。
</Callout>
## 常见问题
#### 数据库迁移问题
@@ -324,37 +341,6 @@ sudo rm -rf ./data # 移除挂载的数据库数据
docker compose up -d # 重新启动
```
#### 使用 `INTERNAL_APP_URL` 配置内部服务器通信
<Callout type="info">
如果你在 CDN如 Cloudflare或反向代理后部署 LobeHub你可以配置内部服务器到服务器通信以绕过 CDN / 代理层,以获得更好的性能。
</Callout>
你可以配置 `INTERNAL_APP_URL` 环境变量:
```yaml
environment:
- 'APP_URL=https://lobe.example.com' # 浏览器访问的公开 URL
- 'INTERNAL_APP_URL=http://localhost:3210' # 服务器到服务器调用的内部 URL
```
**工作原理:**
- `APP_URL`:用于浏览器 / 客户端访问、OAuth 回调、webhook 等(通过 CDN / 代理)
- `INTERNAL_APP_URL`:用于内部服务器到服务器通信(绕过 CDN / 代理)
如果未设置 `INTERNAL_APP_URL`,它将默认为 `APP_URL`。
**配置选项:**
- `http://localhost:3210` - 如果使用 Docker 主机网络模式
- `http://lobe:3210` - 如果使用 Docker 网络与服务名称
- `http://127.0.0.1:3210` - 备用本地主机地址
<Callout type="tip">
对于 Docker Compose 部署,推荐使用 `http://lobe:3210` 作为 `INTERNAL_APP_URL`(使用服务名称进行容器间通信)。
</Callout>
## 反向代理配置
在生产环境中,如需使用自定义域名和 HTTPS需在 LobeHub 前面配置反向代理。

View File

@@ -96,9 +96,9 @@ describe('ssrfSafeFetch', () => {
throw new Error('getaddrinfo ENOTFOUND');
});
await expect(ssrfSafeFetch(url)).rejects.toThrow(/SSRF-safe fetch failed/);
await expect(ssrfSafeFetch(url)).rejects.toThrow(/Fetch failed/);
expect(console.error).toHaveBeenCalledWith('SSRF-safe fetch error:', expect.any(Error));
expect(console.error).toHaveBeenCalledWith('Fetch error:', expect.any(Error));
});
});
@@ -128,21 +128,36 @@ describe('ssrfSafeFetch', () => {
});
describe('SSRF protection for malicious URLs', () => {
const maliciousUrls = [
const privateHttpUrls = [
'http://169.254.169.254/latest/meta-data/', // AWS metadata service
'http://169.254.169.254:80/computeMetadata/v1/', // GCP metadata
'http://metadata.google.internal/computeMetadata/v1/',
];
privateHttpUrls.forEach((url) => {
it(`should SSRF-block private HTTP URL: ${url}`, async () => {
mockFetch.mockImplementation(() => {
throw new Error(
'DNS lookup 169.254.169.254 is not allowed. Because, It is private IP address.',
);
});
await expect(ssrfSafeFetch(url)).rejects.toThrow(/SSRF blocked/);
});
});
const unsupportedSchemeUrls = [
'file:///etc/passwd', // File protocol
'ftp://internal.company.com/secrets', // FTP protocol
];
maliciousUrls.forEach((url) => {
it(`should block malicious URL: ${url}`, async () => {
unsupportedSchemeUrls.forEach((url) => {
it(`should reject unsupported scheme: ${url}`, async () => {
mockFetch.mockImplementation(() => {
throw new Error('Request blocked by SSRF protection');
throw new TypeError('Only HTTP(S) protocols are supported');
});
await expect(ssrfSafeFetch(url)).rejects.toThrow(/SSRF-safe fetch failed/);
await expect(ssrfSafeFetch(url)).rejects.toThrow(/Fetch failed/);
});
});
});
@@ -155,9 +170,7 @@ describe('ssrfSafeFetch', () => {
throw new Error('getaddrinfo ENOTFOUND');
});
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(
/SSRF-safe fetch failed/,
);
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(/Fetch failed/);
});
it('should handle invalid environment variable values gracefully', async () => {
@@ -168,9 +181,7 @@ describe('ssrfSafeFetch', () => {
});
// Should default to false when env var is not 'true'
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(
/SSRF-safe fetch failed/,
);
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(/Fetch failed/);
});
});
@@ -180,10 +191,21 @@ describe('ssrfSafeFetch', () => {
mockFetch.mockRejectedValue(originalError);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: Network error',
'Fetch failed: Network error',
);
expect(console.error).toHaveBeenCalledWith('SSRF-safe fetch error:', originalError);
expect(console.error).toHaveBeenCalledWith('Fetch error:', originalError);
});
it('should throw SSRF blocked error when request-filtering-agent blocks', async () => {
const ssrfError = new Error(
'DNS lookup 10.0.0.1(family:4, host:10.0.0.1) is not allowed. Because, It is private IP address.',
);
mockFetch.mockRejectedValue(ssrfError);
await expect(ssrfSafeFetch('http://10.0.0.1/internal')).rejects.toThrow(/SSRF blocked/);
expect(console.error).toHaveBeenCalledWith('SSRF protection blocked request:', ssrfError);
});
it('should handle non-Error thrown values', async () => {
@@ -191,16 +213,14 @@ describe('ssrfSafeFetch', () => {
mockFetch.mockRejectedValue(nonErrorValue);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: String error',
'Fetch failed: String error',
);
});
it('should handle null/undefined error values', async () => {
mockFetch.mockRejectedValue(null);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: null',
);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow('Fetch failed: null');
});
});

View File

@@ -61,10 +61,20 @@ export const ssrfSafeFetch = async (
statusText: response.statusText,
});
} catch (error) {
console.error('SSRF-safe fetch error:', error);
throw new Error(
`SSRF-safe fetch failed: ${error instanceof Error ? error.message : String(error)}. ` +
'See: https://lobehub.com/docs/self-hosting/environment-variables/basic#ssrf-allow-private-ip-address',
);
const errorMessage = error instanceof Error ? error.message : String(error);
// request-filtering-agent errors contain "is not allowed" when blocking private/denied IPs
const isSSRFBlock = errorMessage.includes('is not allowed');
if (isSSRFBlock) {
console.error('SSRF protection blocked request:', error);
throw new Error(
`SSRF blocked: ${errorMessage}. ` +
'See: https://lobehub.com/docs/self-hosting/environment-variables/basic#ssrf-allow-private-ip-address',
{ cause: error },
);
}
console.error('Fetch error:', error);
throw new Error(`Fetch failed: ${errorMessage}`, { cause: error });
}
};