diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 000000000..d31daf6fd --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-14 - Loose Loopback Validation +**Vulnerability:** `isLoopbackBindHost` incorrectly identified any hostname starting with "127." (e.g., `127.example.com`) as a loopback address. This could bypass the automatic API token generation for non-loopback binds (`ensureApiTokenForBindHost`), potentially exposing the API without authentication if a user binds to such a hostname that resolves to a public IP. +**Learning:** String prefix matching is insufficient for validating loopback addresses when hostnames are involved. Public DNS records can point to public IPs while having names that mimic private IP ranges. +**Prevention:** Always validate resolved IP addresses or use strict IP matching logic (like `net.isIP`) when enforcing network security policies based on address ranges. Do not rely on hostname patterns for security decisions. diff --git a/src/api/server.api-token-bind.test.ts b/src/api/server.api-token-bind.test.ts index b5e217da3..5996464b9 100644 --- a/src/api/server.api-token-bind.test.ts +++ b/src/api/server.api-token-bind.test.ts @@ -45,4 +45,14 @@ describe("ensureApiTokenForBindHost", () => { false, ); }); + + it("generates a token for non-loopback binds starting with 127.", () => { + delete process.env.MILADY_API_TOKEN; + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); + + ensureApiTokenForBindHost("127.example.com"); + + const generated = process.env.MILADY_API_TOKEN ?? ""; + expect(generated).toMatch(/^[a-f0-9]{64}$/); + }); }); diff --git a/src/api/server.ts b/src/api/server.ts index 63819fc20..6a9312883 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -4317,7 +4317,7 @@ function isLoopbackBindHost(host: string): boolean { ) { return true; } - if (normalized.startsWith("127.")) return true; + if (net.isIP(normalized) === 4 && normalized.startsWith("127.")) return true; return false; }