mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 01:22:57 +00:00
Harden hostname normalization for repeated trailing dots [AI] (#87305)
* fix: canonicalize trailing hostname dots * test: reuse shared hostname normalization * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
12dc398267
commit
0314d67d87
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Harden hostname normalization for repeated trailing dots [AI]. (#87305) Thanks @pgondhi987.
|
||||
- fix: block side-effecting command wrappers [AI]. (#87292) Thanks @pgondhi987.
|
||||
- Block unsafe Node runtime env overrides [AI]. (#87308) Thanks @pgondhi987.
|
||||
- Telegram: route `sendMessage` action replies through durable outbound delivery so completed agent responses remain retryable when the gateway send path times out. (#87261) Thanks @mbelinky.
|
||||
|
||||
@@ -2003,6 +2003,31 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"http://localhost.../resource",
|
||||
"http://metadata.google.internal.../computeMetadata/v1/",
|
||||
"http://api.localhost.../resource",
|
||||
"http://svc.local.../resource",
|
||||
"http://db.internal.../resource",
|
||||
])("blocks reserved repeated-dot hostname in trusted env proxy mode %s", async (url) => {
|
||||
clearProxyEnv();
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
const lookupFn = createPublicLookup();
|
||||
const fetchImpl = vi.fn(async () => okResponse());
|
||||
|
||||
await expect(
|
||||
fetchWithSsrFGuard({
|
||||
url,
|
||||
fetchImpl,
|
||||
lookupFn,
|
||||
mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY,
|
||||
}),
|
||||
).rejects.toThrow(/blocked/i);
|
||||
|
||||
expect(lookupFn).not.toHaveBeenCalled();
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps DNS pinning in trusted proxy mode when only ALL_PROXY is configured after allowlist checks", async () => {
|
||||
clearProxyEnv();
|
||||
vi.stubEnv("ALL_PROXY", "http://127.0.0.1:7890");
|
||||
|
||||
@@ -4,8 +4,11 @@ import { normalizeHostname } from "./hostname.js";
|
||||
describe("normalizeHostname", () => {
|
||||
it.each([
|
||||
{ input: " Example.COM. ", expected: "example.com" },
|
||||
{ input: " Example.COM... ", expected: "example.com" },
|
||||
{ input: "metadata.google.internal...", expected: "metadata.google.internal" },
|
||||
{ input: " ", expected: "" },
|
||||
{ input: " [FD7A:115C:A1E0::1] ", expected: "fd7a:115c:a1e0::1" },
|
||||
{ input: " [FD7A:115C:A1E0::1]... ", expected: "fd7a:115c:a1e0::1" },
|
||||
{ input: " [FD7A:115C:A1E0::1]. ", expected: "fd7a:115c:a1e0::1" },
|
||||
{ input: "[fd7a:115c:a1e0::1", expected: "[fd7a:115c:a1e0::1" },
|
||||
{ input: "fd7a:115c:a1e0::1]", expected: "fd7a:115c:a1e0::1]" },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
export function normalizeHostname(hostname: string): string {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
|
||||
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.+$/, "");
|
||||
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
||||
return normalized.slice(1, -1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { blockedIpv6MulticastLiterals } from "../../shared/net/ip-test-fixtures.js";
|
||||
import {
|
||||
assertHostnameAllowedWithPolicy,
|
||||
isBlockedHostnameOrIp,
|
||||
isPrivateIpAddress,
|
||||
isSameSsrFPolicy,
|
||||
@@ -267,6 +268,18 @@ describe("isBlockedHostnameOrIp", () => {
|
||||
expect(isBlockedHostnameOrIp(hostname)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"localhost...",
|
||||
"localhost.localdomain...",
|
||||
"metadata.google.internal...",
|
||||
"api.localhost...",
|
||||
"svc.local...",
|
||||
"db.internal...",
|
||||
])("blocks reserved hostname with repeated trailing dots %s", (hostname) => {
|
||||
expect(isBlockedHostnameOrIp(hostname)).toBe(true);
|
||||
expect(() => assertHostnameAllowedWithPolicy(hostname)).toThrow(/blocked/i);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["2001:db8:1234::5efe:127.0.0.1", true],
|
||||
["100::1", true],
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { vi } from "vitest";
|
||||
import { normalizeHostname } from "../infra/net/hostname.js";
|
||||
import * as ssrf from "../infra/net/ssrf.js";
|
||||
import type { LookupFn } from "../infra/net/ssrf.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
|
||||
export function mockPinnedHostnameResolution(addresses: string[] = ["93.184.216.34"]) {
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
const resolvePinnedHostnameWithPolicy = ssrf.resolvePinnedHostnameWithPolicy;
|
||||
const lookupFn = (async (hostname: string, options?: { all?: boolean }) => {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
|
||||
const normalized = normalizeHostname(hostname);
|
||||
const resolved = addresses.map((address) => ({
|
||||
address,
|
||||
family: address.includes(":") ? 6 : 4,
|
||||
|
||||
Reference in New Issue
Block a user