mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-28 00:43:57 +00:00
fix(browser): fail closed navigation guard with env proxy
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { SsrFBlockedError, type LookupFn } from "../infra/net/ssrf.js";
|
import { SsrFBlockedError, type LookupFn } from "../infra/net/ssrf.js";
|
||||||
import {
|
import {
|
||||||
assertBrowserNavigationAllowed,
|
assertBrowserNavigationAllowed,
|
||||||
@@ -12,6 +12,10 @@ function createLookupFn(address: string): LookupFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("browser navigation guard", () => {
|
describe("browser navigation guard", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
it("blocks private loopback URLs by default", async () => {
|
it("blocks private loopback URLs by default", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
assertBrowserNavigationAllowed({
|
assertBrowserNavigationAllowed({
|
||||||
@@ -95,6 +99,29 @@ describe("browser navigation guard", () => {
|
|||||||
expect(lookupFn).toHaveBeenCalledWith("example.com", { all: true });
|
expect(lookupFn).toHaveBeenCalledWith("example.com", { all: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks strict policy navigation when env proxy is configured", async () => {
|
||||||
|
vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||||
|
const lookupFn = createLookupFn("93.184.216.34");
|
||||||
|
await expect(
|
||||||
|
assertBrowserNavigationAllowed({
|
||||||
|
url: "https://example.com",
|
||||||
|
lookupFn,
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows env proxy navigation when private-network mode is explicitly enabled", async () => {
|
||||||
|
vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||||
|
const lookupFn = createLookupFn("93.184.216.34");
|
||||||
|
await expect(
|
||||||
|
assertBrowserNavigationAllowed({
|
||||||
|
url: "https://example.com",
|
||||||
|
lookupFn,
|
||||||
|
ssrfPolicy: { dangerouslyAllowPrivateNetwork: true },
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects invalid URLs", async () => {
|
it("rejects invalid URLs", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
assertBrowserNavigationAllowed({
|
assertBrowserNavigationAllowed({
|
||||||
|
|||||||
@@ -6,6 +6,28 @@ import {
|
|||||||
|
|
||||||
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
||||||
const SAFE_NON_NETWORK_URLS = new Set(["about:blank"]);
|
const SAFE_NON_NETWORK_URLS = new Set(["about:blank"]);
|
||||||
|
const ENV_PROXY_KEYS = [
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"ALL_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
"https_proxy",
|
||||||
|
"all_proxy",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function hasEnvProxyConfigured(): boolean {
|
||||||
|
for (const key of ENV_PROXY_KEYS) {
|
||||||
|
const value = process.env[key];
|
||||||
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowsPrivateNetworkNavigation(policy?: SsrFPolicy): boolean {
|
||||||
|
return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
|
||||||
|
}
|
||||||
|
|
||||||
function isAllowedNonNetworkNavigationUrl(parsed: URL): boolean {
|
function isAllowedNonNetworkNavigationUrl(parsed: URL): boolean {
|
||||||
// Keep non-network navigation explicit; about:blank is the only allowed bootstrap URL.
|
// Keep non-network navigation explicit; about:blank is the only allowed bootstrap URL.
|
||||||
@@ -56,6 +78,16 @@ export async function assertBrowserNavigationAllowed(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Browser network stacks may apply env proxy routing at connect-time, which
|
||||||
|
// can bypass strict destination-binding intent from pre-navigation DNS checks.
|
||||||
|
// In strict mode, fail closed unless private-network navigation is explicitly
|
||||||
|
// enabled by policy.
|
||||||
|
if (hasEnvProxyConfigured() && !allowsPrivateNetworkNavigation(opts.ssrfPolicy)) {
|
||||||
|
throw new InvalidBrowserNavigationUrlError(
|
||||||
|
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
||||||
lookupFn: opts.lookupFn,
|
lookupFn: opts.lookupFn,
|
||||||
policy: opts.ssrfPolicy,
|
policy: opts.ssrfPolicy,
|
||||||
|
|||||||
Reference in New Issue
Block a user