mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: distinguish SSRF block errors from network errors (#13103)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 前面配置反向代理。
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user