mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 10:51:16 +00:00
fix(litellm): guard loopback hostname auto-allow with isIP to prevent DNS SSRF bypass (#110693)
* fix(litellm): guard loopback hostname auto-allow with isIP to prevent DNS bypass
The isAutoAllowedLitellmHostname helper auto-enables private-network access
for loopback-style hosts. Before this fix, lowered.startsWith("127.")
matched DNS hostnames like 127.evil.com, letting remote endpoints bypass
the explicit allowPrivateNetwork opt-in — a SSRF risk.
Add isIP(host)===4 guard so only literal IPv4 loopback addresses qualify.
Same canonical pattern as extensions/slack/src/monitor/relay-source.ts:271
and the codex loopback fix.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(litellm): cover loopback endpoint policy
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -254,7 +254,9 @@ describe("litellm image generation provider", () => {
|
||||
const cases = [
|
||||
"http://localhost:4000",
|
||||
"http://127.0.0.1:4000",
|
||||
"http://127.255.255.254:4000",
|
||||
"http://[::1]:4000",
|
||||
"http://[0:0:0:0:0:0:0:1]:4000",
|
||||
"http://host.docker.internal:4000",
|
||||
"https://localhost:4000",
|
||||
] as const;
|
||||
@@ -283,6 +285,7 @@ describe("litellm image generation provider", () => {
|
||||
"https://192.168.5.10:4000",
|
||||
"http://printer.local:4000",
|
||||
"http://proxy.internal:4000",
|
||||
"http://127.evil.com:4000",
|
||||
"https://metadata.google.internal",
|
||||
] as const;
|
||||
for (const baseUrl of cases) {
|
||||
@@ -303,33 +306,36 @@ describe("litellm image generation provider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("honors explicit private-network opt-in for a LAN LiteLLM proxy", async () => {
|
||||
mockGeneratedPngResponse();
|
||||
it.each(["http://192.168.5.10:4000", "http://127.evil.com:4000"])(
|
||||
"honors explicit private-network opt-in for %s",
|
||||
async (baseUrl) => {
|
||||
mockGeneratedPngResponse();
|
||||
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl: "http://192.168.5.10:4000",
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [],
|
||||
const provider = buildLitellmImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "litellm",
|
||||
model: "gpt-image-2",
|
||||
prompt: "x",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
baseUrl,
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expectFields(mockObjectArg(resolveProviderHttpRequestConfigMock), {
|
||||
allowPrivateNetwork: undefined,
|
||||
request: { allowPrivateNetwork: true },
|
||||
});
|
||||
expect(mockObjectArg(postJsonRequestMock).allowPrivateNetwork).toBe(true);
|
||||
});
|
||||
expectFields(mockObjectArg(resolveProviderHttpRequestConfigMock), {
|
||||
allowPrivateNetwork: undefined,
|
||||
request: { allowPrivateNetwork: true },
|
||||
});
|
||||
expect(mockObjectArg(postJsonRequestMock).allowPrivateNetwork).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not allow private network for public hosts that embed private strings in the URL", async () => {
|
||||
// Must not be fooled by an attacker-controlled URL that mentions
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isIP } from "node:net";
|
||||
// Litellm provider module implements model/runtime integration.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
@@ -62,7 +63,8 @@ function isAutoAllowedLitellmHostname(hostname: string): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (lowered === "127.0.0.1" || lowered.startsWith("127.")) {
|
||||
// Only IPv4 literals may use the 127/8 loopback exemption.
|
||||
if (isIP(lowered) === 4 && lowered.startsWith("127.")) {
|
||||
return true;
|
||||
}
|
||||
if (lowered === "::1" || lowered === "0:0:0:0:0:0:0:1") {
|
||||
|
||||
Reference in New Issue
Block a user