보안 URL 가져오기

보안 URL 가져오기 (Secure URL Fetching)

많은 provider가 응답 본문에 URL을 반환해요 — 생성된 이미지, 오디오, 비디오를 다운로드하거나 작업 상태를 확인할 폴링 URL 같은 거예요. AI SDK는 이를 서버 쪽에서 가져와(fetch) 결과를 코드에 반환해요. 그 URL은 외부 서비스에서 오는 것이므로, 악의적이거나 손상된 provider(또는 응답을 변조할 수 있는 누구든)가 그 URL을 클라우드 메타데이터 엔드포인트(http://169.254.169.254/…), 프라이빗 호스트(http://10.0.0.5/…), 또는 localhost 같은 내부 주소로 가리킬 수 있어요.

이를 막기 위해 SDK는 response가 제공한 모든 URL을 가져오기 전에 검증해요. 이는 provider 패키지 안에서 자동으로 일어나므로, 별도로 구성할 필요가 없어요.

인증이 필요한 작업 상태 폴링의 경우, provider는 구성된 API 엔드포인트에서 첫 URL을 만들 수 있어요. SDK는 초기 요청에 대해 그 구성된 origin을 신뢰하지만, 그로부터의 모든 리다이렉트는 수동으로 따라가며 검증해요. MiniMax, Kling AI, ByteDance 비디오 폴링이 이 보호된 경로를 사용해요.

출처: 문서

본문

SDK가 보호하는 것 (What the SDK protects against)

SDK가 provider 응답에서 가져온 URL을 fetch할 때 다음을 수행해요:

  • 프라이빗, 루프백, 링크-로컬 대상 거부 — IPv4(10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, CGNAT, 멀티캐스트, …)와 동등한 IPv6 범위, 그리고 localhost와 .local. http(s)가 아닌 스킴도 거부돼요.
  • 모든 리다이렉트 홉을 재검증 — 통과한 URL이라도 내부 주소로 리다이렉트되면 차단되고, 리다이렉트를 맹목적으로 따라가지 않아요.
  • Node.js에서는 연결 시점에 DNS 검증 — 모든 해석된 주소를 확인하고, 소켓을 검증된 DNS 결과에 고정해서 DNS 리바인딩이 검증과 연결 사이에 다른 주소를 도입하지 못하게 해요.
  • 위험한 요청 헤더 제거 — 프록시 전달, 클라우드 메타데이터, 쿠키 헤더를 요청 전에 제거해요.
  • 크로스 오리진에서 자격 증명 제거 — 호출자 헤더(Authorization, Cookie, provider별 API 키 헤더 모두)를 provider와 다른 origin의 호스트로 보내지 않아요. origin을 가로지르는 리다이렉트는 user-agent를 제외한 모든 것을 제거해요.

차단된 URL은 DownloadError로 표면화돼요.

신뢰할 수 없는 URL 직접 가져오기 (Fetching untrusted URLs directly)

@ai-sdk/provider-utils를 사용하는 커스텀 통합에서는, URL이 응답이나 다른 신뢰할 수 없는 입력에서 오는 경우 fetchUntrustedUrl을 사용하세요. URL과 리다이렉트를 검증하고, URL이 구성된 origin과 일치하지 않으면 첫 요청에서 자격 증명과 알 수 없는 커스텀 헤더를 보류해요:

import { fetchUntrustedUrl } from '@ai-sdk/provider-utils';

const response = await fetchUntrustedUrl({
  url: result.downloadUrl,
  headers: {
    authorization: *** ${apiKey}`,
    accept: 'image/*',
  },
  credentialedOrigin: providerBaseURL,
});

여기서 Authorization은 초기 URL이 providerBaseURL과 같은 origin일 때만 전송돼요. 그렇지 않으면 Accept, Range, User-Agent 같은 허용 목록 메타데이터만 전송돼요. 크로스 오리진 리다이렉트는 User-Agent를 제외한 모든 호출자 헤더를 제거하고, 이후 홉에서는 자격 증명을 다시 붙이지 않아요.

credentialedOrigin은 구성에서 가져와야 하며, 응답 URL에서 가져오면 안 돼요. URL 검증을 비활성화하지 않아요. 구성된 프라이빗 엔드포인트라면 trustedOrigin도 함께 전달하세요. credentialedOrigin을 생략하면 fetchUntrustedUrl은 trustedOrigin을 자격 증명을 받을 수 있는 origin으로 사용해요. 추가 프로토콜 메타데이터는 untrustedFirstHopHeaders: ['x-protocol-version']로 허용할 수 있어요. 어떤 대상에도 공개해도 안전한 헤더만 나열하고, 자격 증명에는 구성된 origin을 사용하세요.

기존 fetchWithValidatedRedirects 헬퍼는 첫 요청 동작을 유지해요: 위험한 헤더를 정리하지만 인증과 커스텀 헤더는 계속 전달해요. 자격 증명 분리를 첫 요청에 적용하려면 fetchUntrustedUrl로 전환하세요. getFromApi에 대한 기존 호출도 동작을 유지하므로, 자격 증명으로 response가 제공한 URL을 가져올 때는 credentialedOrigin을 전달하세요.

셀프 호스팅 및 로컬 엔드포인트 (Self-hosted and local endpoints)

사용자가 구성한 provider 엔드포인트와 같은 origin의 URL(예: 셀프 호스팅 또는 localhost 배포를 가리키는 커스텀 baseURL)은 이 검사에서 면제돼요 — 정확히 SDK에 알려준 호스트를 대상으로 하기 때문이에요. 이는 작업 상태 폴링에도 적용돼요. 그 origin에서 벗어나는 모든 리다이렉트는 리다이렉트 요청을 보내기 전에 여전히 검증돼요.

런타임별 DNS 검증 (DNS validation across runtimes)

Node.js에서 기본 검증된 다운로드 fetch는 node:dns와 undici 커넥터 훅을 사용해 연결 시점에 해석된 모든 주소를 검증해요. 커넥터는 그 정확한 결과를 사용하므로 호스트 이름→프라이빗 IP 우회와 DNS 리바인딩 우회를 모두 차단해요.

전역 fetch를 감싸거나 교체해도 이 보호는 비활성화되지 않아요: 기본 Node.js 다운로드 전송은 전역 fetch와 독립적이에요.

Bun, Deno, Cloudflare Workers, 프레임워크 엣지 런타임은 Node 호환 process 객체를 노출해도 플랫폼 fetch를 사용해요.

커스텀 fetch를 명시적으로 주입하면, 그 fetch가 동등한 DNS 검증과 연결 고정을 책임져야 해요. 다른 런타임은 Node의 DNS/소켓 훅을 노출하지 않으므로, 그런 런타임의 서버 배포는 프라이빗, 루프백, 링크-로컬, 클라우드 메타데이터 범위로의 네트워크 이그레스(egress)를 제한해야 해요.

배포 강화하기 (Hardening your deployment)

서버가 provider가 제공한 URL을 가져오고 DNS 격차를 막고 싶다면 다음 중 하나(이상적으로는 둘 다)를 사용하세요:

1. 네트워크 계층에서 아웃바운드 이그레스 제한

서버의 네트워크 이그레스에서 169.254.0.0/16, RFC-1918 범위, 루프백을 거부하세요. 이것이 가장 견고한 제어이며 애플리케이션 코드와 독립적이에요.

2. 주입된 fetch 강화

Node.js 기본값은 이미 고정되어 있어요. 커스텀 fetch를 명시적으로 주입한다면, connect.lookup이 해석된 IP를 검증하고 소켓이 안전한 주소에만 연결하도록 하는 undici Agent로 뒷받침해서, 호스트 이름→프라이빗 우회와 DNS 리바인딩 창을 모두 닫으세요:

import { Agent, fetch as undiciFetch } from 'undici';
import { lookup } from 'node:dns';

// Your own check that returns true for private/loopback/link-local addresses.
declare function isUnsafeAddress(ip: string): boolean;

const safeLookup: typeof lookup = (hostname, options, callback) => {
  lookup(hostname, options as any, (err, address, family) => {
    if (!err && typeof address === 'string' && isUnsafeAddress(address)) {
      callback(new Error(`Refusing to connect to ${address}`), '', 0);
      return;
    }
    (callback as any)(err, address, family);
  });
};

const safeDispatcher = new Agent({ connect: { lookup: safeLookup } });

const safeFetch: typeof fetch = (input, init) =>
  undiciFetch(input, { ...init, dispatcher: safeDispatcher }) as any;
import { createFal } from '@ai-sdk/fal';

const fal = createFal({ fetch: safeFetch });

SDK의 URL 검증과 커스텀 fetch의 연결 시점 고정은 상호 보완적이니, 둘 다 유지하세요.

더 알아보기 (Learn more)