Harden hostname normalization for repeated trailing dots [AI] (#87305) · openclaw/openclaw@0314d67
pgondhi987
·
2026-05-27
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
11 | 11 | |
12 | 12 | ### Fixes |
13 | 13 | |
| 14 | +- Harden hostname normalization for repeated trailing dots [AI]. (#87305) Thanks @pgondhi987. |
14 | 15 | - fix: block side-effecting command wrappers [AI]. (#87292) Thanks @pgondhi987. |
15 | 16 | - Block unsafe Node runtime env overrides [AI]. (#87308) Thanks @pgondhi987. |
16 | 17 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2003,6 +2003,31 @@ describe("fetchWithSsrFGuard hardening", () => {
|
2003 | 2003 | expect(fetchImpl).not.toHaveBeenCalled(); |
2004 | 2004 | }); |
2005 | 2005 | |
| 2006 | +it.each([ |
| 2007 | +"http://localhost.../resource", |
| 2008 | +"http://metadata.google.internal.../computeMetadata/v1/", |
| 2009 | +"http://api.localhost.../resource", |
| 2010 | +"http://svc.local.../resource", |
| 2011 | +"http://db.internal.../resource", |
| 2012 | +])("blocks reserved repeated-dot hostname in trusted env proxy mode %s", async (url) => { |
| 2013 | +clearProxyEnv(); |
| 2014 | +vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890"); |
| 2015 | +const lookupFn = createPublicLookup(); |
| 2016 | +const fetchImpl = vi.fn(async () => okResponse()); |
| 2017 | + |
| 2018 | +await expect( |
| 2019 | +fetchWithSsrFGuard({ |
| 2020 | + url, |
| 2021 | + fetchImpl, |
| 2022 | + lookupFn, |
| 2023 | +mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, |
| 2024 | +}), |
| 2025 | +).rejects.toThrow(/blocked/i); |
| 2026 | + |
| 2027 | +expect(lookupFn).not.toHaveBeenCalled(); |
| 2028 | +expect(fetchImpl).not.toHaveBeenCalled(); |
| 2029 | +}); |
| 2030 | + |
2006 | 2031 | it("keeps DNS pinning in trusted proxy mode when only ALL_PROXY is configured after allowlist checks", async () => { |
2007 | 2032 | clearProxyEnv(); |
2008 | 2033 | vi.stubEnv("ALL_PROXY", "http://127.0.0.1:7890"); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4,8 +4,11 @@ import { normalizeHostname } from "./hostname.js";
|
4 | 4 | describe("normalizeHostname", () => { |
5 | 5 | it.each([ |
6 | 6 | { input: " Example.COM. ", expected: "example.com" }, |
| 7 | +{ input: " Example.COM... ", expected: "example.com" }, |
| 8 | +{ input: "metadata.google.internal...", expected: "metadata.google.internal" }, |
7 | 9 | { input: " ", expected: "" }, |
8 | 10 | { input: " [FD7A:115C:A1E0::1] ", expected: "fd7a:115c:a1e0::1" }, |
| 11 | +{ input: " [FD7A:115C:A1E0::1]... ", expected: "fd7a:115c:a1e0::1" }, |
9 | 12 | { input: " [FD7A:115C:A1E0::1]. ", expected: "fd7a:115c:a1e0::1" }, |
10 | 13 | { input: "[fd7a:115c:a1e0::1", expected: "[fd7a:115c:a1e0::1" }, |
11 | 14 | { input: "fd7a:115c:a1e0::1]", expected: "fd7a:115c:a1e0::1]" }, |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; |
2 | 2 | |
3 | 3 | export function normalizeHostname(hostname: string): string { |
4 | | -const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, ""); |
| 4 | +const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.+$/, ""); |
5 | 5 | if (normalized.startsWith("[") && normalized.endsWith("]")) { |
6 | 6 | return normalized.slice(1, -1); |
7 | 7 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | 2 | import { blockedIpv6MulticastLiterals } from "../../shared/net/ip-test-fixtures.js"; |
3 | 3 | import { |
| 4 | +assertHostnameAllowedWithPolicy, |
4 | 5 | isBlockedHostnameOrIp, |
5 | 6 | isPrivateIpAddress, |
6 | 7 | isSameSsrFPolicy, |
@@ -267,6 +268,18 @@ describe("isBlockedHostnameOrIp", () => {
|
267 | 268 | expect(isBlockedHostnameOrIp(hostname)).toBe(true); |
268 | 269 | }); |
269 | 270 | |
| 271 | +it.each([ |
| 272 | +"localhost...", |
| 273 | +"localhost.localdomain...", |
| 274 | +"metadata.google.internal...", |
| 275 | +"api.localhost...", |
| 276 | +"svc.local...", |
| 277 | +"db.internal...", |
| 278 | +])("blocks reserved hostname with repeated trailing dots %s", (hostname) => { |
| 279 | +expect(isBlockedHostnameOrIp(hostname)).toBe(true); |
| 280 | +expect(() => assertHostnameAllowedWithPolicy(hostname)).toThrow(/blocked/i); |
| 281 | +}); |
| 282 | + |
270 | 283 | it.each([ |
271 | 284 | ["2001:db8:1234::5efe:127.0.0.1", true], |
272 | 285 | ["100::1", true], |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { vi } from "vitest"; |
| 2 | +import { normalizeHostname } from "../infra/net/hostname.js"; |
2 | 3 | import * as ssrf from "../infra/net/ssrf.js"; |
3 | 4 | import type { LookupFn } from "../infra/net/ssrf.js"; |
4 | | -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; |
5 | 5 | |
6 | 6 | export function mockPinnedHostnameResolution(addresses: string[] = ["93.184.216.34"]) { |
7 | 7 | const resolvePinnedHostname = ssrf.resolvePinnedHostname; |
8 | 8 | const resolvePinnedHostnameWithPolicy = ssrf.resolvePinnedHostnameWithPolicy; |
9 | 9 | const lookupFn = (async (hostname: string, options?: { all?: boolean }) => { |
10 | | -const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, ""); |
| 10 | +const normalized = normalizeHostname(hostname); |
11 | 11 | const resolved = addresses.map((address) => ({ |
12 | 12 | address, |
13 | 13 | family: address.includes(":") ? 6 : 4, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。