Skip to content

Commit 257e049

Browse files
committed
fix redirect case
1 parent 7af5c18 commit 257e049

File tree

1 file changed

+56
-4
lines changed

1 file changed

+56
-4
lines changed

apps/sim/lib/core/security/input-validation.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ export interface SecureFetchOptions {
905905
headers?: Record<string, string>
906906
body?: string
907907
timeout?: number
908+
maxRedirects?: number
908909
}
909910

910911
export class SecureFetchHeaders {
@@ -941,15 +942,33 @@ export interface SecureFetchResponse {
941942
arrayBuffer: () => Promise<ArrayBuffer>
942943
}
943944

945+
const DEFAULT_MAX_REDIRECTS = 5
946+
947+
function isRedirectStatus(status: number): boolean {
948+
return status >= 300 && status < 400 && status !== 304
949+
}
950+
951+
function resolveRedirectUrl(baseUrl: string, location: string): string {
952+
try {
953+
return new URL(location, baseUrl).toString()
954+
} catch {
955+
throw new Error(`Invalid redirect location: ${location}`)
956+
}
957+
}
958+
944959
/**
945960
* Performs a fetch with IP pinning to prevent DNS rebinding attacks.
946961
* Uses the pre-resolved IP address while preserving the original hostname for TLS SNI.
962+
* Follows redirects securely by validating each redirect target.
947963
*/
948-
export function secureFetchWithPinnedIP(
964+
export async function secureFetchWithPinnedIP(
949965
url: string,
950966
resolvedIP: string,
951-
options: SecureFetchOptions = {}
967+
options: SecureFetchOptions = {},
968+
redirectCount = 0
952969
): Promise<SecureFetchResponse> {
970+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS
971+
953972
return new Promise((resolve, reject) => {
954973
const parsed = new URL(url)
955974
const isHttps = parsed.protocol === 'https:'
@@ -985,6 +1004,39 @@ export function secureFetchWithPinnedIP(
9851004

9861005
const protocol = isHttps ? https : http
9871006
const req = protocol.request(requestOptions, (res) => {
1007+
const statusCode = res.statusCode || 0
1008+
const location = res.headers.location
1009+
1010+
if (isRedirectStatus(statusCode) && location && redirectCount < maxRedirects) {
1011+
res.resume()
1012+
const redirectUrl = resolveRedirectUrl(url, location)
1013+
1014+
validateUrlWithDNS(redirectUrl, 'redirectUrl')
1015+
.then((validation) => {
1016+
if (!validation.isValid) {
1017+
reject(new Error(`Redirect blocked: ${validation.error}`))
1018+
return
1019+
}
1020+
return secureFetchWithPinnedIP(
1021+
redirectUrl,
1022+
validation.resolvedIP!,
1023+
options,
1024+
redirectCount + 1
1025+
)
1026+
})
1027+
.then((response) => {
1028+
if (response) resolve(response)
1029+
})
1030+
.catch(reject)
1031+
return
1032+
}
1033+
1034+
if (isRedirectStatus(statusCode) && location && redirectCount >= maxRedirects) {
1035+
res.resume()
1036+
reject(new Error(`Too many redirects (max: ${maxRedirects})`))
1037+
return
1038+
}
1039+
9881040
const chunks: Buffer[] = []
9891041

9901042
res.on('data', (chunk: Buffer) => chunks.push(chunk))
@@ -1006,8 +1058,8 @@ export function secureFetchWithPinnedIP(
10061058
}
10071059

10081060
resolve({
1009-
ok: res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300,
1010-
status: res.statusCode || 0,
1061+
ok: statusCode >= 200 && statusCode < 300,
1062+
status: statusCode,
10111063
statusText: res.statusMessage || '',
10121064
headers: new SecureFetchHeaders(headersRecord),
10131065
text: async () => body,

0 commit comments

Comments
 (0)