11import { EnvHttpProxyAgent , type Dispatcher } from "undici" ;
22import { logWarn } from "../../logger.js" ;
33import { bindAbortRelay } from "../../utils/fetch-timeout.js" ;
4+ import { hasProxyEnvConfigured } from "./proxy-env.js" ;
45import {
56 closeDispatcher ,
67 createPinnedDispatcher ,
@@ -12,6 +13,13 @@ import {
1213
1314type FetchLike = ( input : RequestInfo | URL , init ?: RequestInit ) => Promise < Response > ;
1415
16+ export const GUARDED_FETCH_MODE = {
17+ STRICT : "strict" ,
18+ TRUSTED_ENV_PROXY : "trusted_env_proxy" ,
19+ } as const ;
20+
21+ export type GuardedFetchMode = ( typeof GUARDED_FETCH_MODE ) [ keyof typeof GUARDED_FETCH_MODE ] ;
22+
1523export type GuardedFetchOptions = {
1624 url : string ;
1725 fetchImpl ?: FetchLike ;
@@ -21,11 +29,12 @@ export type GuardedFetchOptions = {
2129 signal ?: AbortSignal ;
2230 policy ?: SsrFPolicy ;
2331 lookupFn ?: LookupFn ;
32+ mode ?: GuardedFetchMode ;
2433 pinDns ?: boolean ;
34+ /** @deprecated use `mode: "trusted_env_proxy"` for trusted/operator-controlled URLs. */
2535 proxy ?: "env" ;
2636 /**
27- * Env proxies can break destination binding between SSRF pre-check and connect-time target.
28- * Keep this off for untrusted URLs; enable only for trusted/operator-controlled endpoints.
37+ * @deprecated use `mode: "trusted_env_proxy"` instead.
2938 */
3039 dangerouslyAllowEnvProxyWithoutPinnedDns ?: boolean ;
3140 auditContext ?: string ;
@@ -37,30 +46,37 @@ export type GuardedFetchResult = {
3746 release : ( ) => Promise < void > ;
3847} ;
3948
49+ type GuardedFetchPresetOptions = Omit <
50+ GuardedFetchOptions ,
51+ "mode" | "proxy" | "dangerouslyAllowEnvProxyWithoutPinnedDns"
52+ > ;
53+
4054const DEFAULT_MAX_REDIRECTS = 3 ;
41- const ENV_PROXY_KEYS = [
42- "HTTP_PROXY" ,
43- "HTTPS_PROXY" ,
44- "ALL_PROXY" ,
45- "http_proxy" ,
46- "https_proxy" ,
47- "all_proxy" ,
48- ] as const ;
4955const CROSS_ORIGIN_REDIRECT_SENSITIVE_HEADERS = [
5056 "authorization" ,
5157 "proxy-authorization" ,
5258 "cookie" ,
5359 "cookie2" ,
5460] ;
5561
56- function hasEnvProxyConfigured ( ) : boolean {
57- for ( const key of ENV_PROXY_KEYS ) {
58- const value = process . env [ key ] ;
59- if ( typeof value === "string" && value . trim ( ) ) {
60- return true ;
61- }
62+ export function withStrictGuardedFetchMode ( params : GuardedFetchPresetOptions ) : GuardedFetchOptions {
63+ return { ...params , mode : GUARDED_FETCH_MODE . STRICT } ;
64+ }
65+
66+ export function withTrustedEnvProxyGuardedFetchMode (
67+ params : GuardedFetchPresetOptions ,
68+ ) : GuardedFetchOptions {
69+ return { ...params , mode : GUARDED_FETCH_MODE . TRUSTED_ENV_PROXY } ;
70+ }
71+
72+ function resolveGuardedFetchMode ( params : GuardedFetchOptions ) : GuardedFetchMode {
73+ if ( params . mode ) {
74+ return params . mode ;
75+ }
76+ if ( params . proxy === "env" && params . dangerouslyAllowEnvProxyWithoutPinnedDns === true ) {
77+ return GUARDED_FETCH_MODE . TRUSTED_ENV_PROXY ;
6278 }
63- return false ;
79+ return GUARDED_FETCH_MODE . STRICT ;
6480}
6581
6682function isRedirectStatus ( status : number ) : boolean {
@@ -122,6 +138,7 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
122138 typeof params . maxRedirects === "number" && Number . isFinite ( params . maxRedirects )
123139 ? Math . max ( 0 , Math . floor ( params . maxRedirects ) )
124140 : DEFAULT_MAX_REDIRECTS ;
141+ const mode = resolveGuardedFetchMode ( params ) ;
125142
126143 const { signal, cleanup } = buildAbortSignal ( {
127144 timeoutMs : params . timeoutMs ,
@@ -162,8 +179,9 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
162179 lookupFn : params . lookupFn ,
163180 policy : params . policy ,
164181 } ) ;
165- const hasEnvProxy = params . proxy === "env" && hasEnvProxyConfigured ( ) ;
166- if ( hasEnvProxy && params . dangerouslyAllowEnvProxyWithoutPinnedDns === true ) {
182+ const canUseTrustedEnvProxy =
183+ mode === GUARDED_FETCH_MODE . TRUSTED_ENV_PROXY && hasProxyEnvConfigured ( ) ;
184+ if ( canUseTrustedEnvProxy ) {
167185 dispatcher = new EnvHttpProxyAgent ( ) ;
168186 } else if ( params . pinDns !== false ) {
169187 dispatcher = createPinnedDispatcher ( pinned ) ;
0 commit comments