Files
2nd/00_Raw/ASTRA SSRF 방어 URL fetch 경계 2026-06-19.md
T
koriweb 8957890d13 위키 동기화 2026-06-19: 보안 트러블슈팅 노트·회의록·lessons·Digests + ASTRA 성장 산출물
- 00_Raw: ASTRA 보안 가이드 3종(SSRF/셸 명령/파일 경로 경계), 회의록 p/q/r 추가
- Topics: Digests 5종, lessons 4종, 메모리 에피소드/장기기억 갱신
- .astra: growth(decay/regression/weakness)·eval(corrections/report) 학습 산출물 갱신

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:29:23 +09:00

8.0 KiB

id, title, category, status, verification_status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, created_at, updated_at, review_reason, merge_history, tags, raw_sources, applied_in, github_commit
id title category status verification_status canonical_id aliases duplicate_of source_trust_level confidence_score created_at updated_at review_reason merge_history tags raw_sources applied_in github_commit
astra-ssrf-url-fetch-guard-20260619 ASTRA SSRF 방어 — 신뢰 불가 URL fetch 경계(assertPublicUrl) Security applied validated
SSRF 방어
assertPublicUrl
ssrfGuard
URL fetch 경계
사설 IP 차단
169.254.169.254 메타데이터 차단
DNS 리바인딩
fetch_url 보안
buildUrlContext 가드
S 0.95 2026-06-19 2026-06-19
security
astra
ssrf
network
fetch
web
troubleshooting
E:/Wiki/astraai/src/features/web/ssrfGuard.ts
E:/Wiki/astraai/src/features/web/webFetch.ts
E:/Wiki/astraai/src/lib/contextBuilders/urlContext.ts
E:/Wiki/astraai/src/agent/actions/webFetch.ts
E:/Wiki/astraai/tests/ssrfGuard.test.ts
E:/Wiki/astraai @ branch (uncommitted, 2026-06-19)

ASTRA SSRF 방어 URL fetch 경계

🎯 한 줄 통찰 (One-line insight)

모델/웹 콘텐츠가 준 URL을 호스트 검증 없이 fetch 하던 SSRF 경로를, "공인 IP 가 아니면 거부"하는 allowlist 성격의 assertPublicUrl 가드로 닫고 리다이렉트 각 홉까지 재검증한다.

🧠 핵심 개념 (Core concepts)

  • SSRF (Server-Side Request Forgery): 신뢰 불가 입력으로 서버가 내부 자원에 요청하게 만드는 공격. 여기선 "에이전트 프로세스"가 내부망/메타데이터/로컬 브릿지에 도달.
  • 차단 대상 IP: 루프백(127/8·::1), 사설(10/8·172.16/12·192.168/16·fc00::/7), 링크로컬(169.254/16=클라우드 메타데이터·fe80::/10), CGNAT/멀티캐스트/예약, IPv4-mapped IPv6.
  • 단일 chokepoint: buildUrlContext 입구에서 한 번 검증 → 브릿지 추출·직접 fetch 양 경로 모두 보호.
  • 리다이렉트 재검증: redirect:'manual' + 각 홉 assertPublicUrl 재호출로 공인→사설 우회 차단.

🩺 증상 (Symptom)

일반 챗의 URL 주입과 에이전트 <fetch_url>이 모델/페이지가 제시한 임의 URL을 redirect:'follow'로 그대로 fetch했다. 악성 페이지가 http://127.0.0.1:3002(로컬 브릿지)·http://169.254.169.254/...(메타데이터)·NAS/인트라넷 주소를 제시하면 그 응답 본문이 모델 컨텍스트로 주입될 수 있었다.

🌐 환경 / 범위 (Environment & scope)

  • 프로젝트: ASTRA (E:/Wiki/astraai). Node 18+ global fetch.
  • 경로: urlContext.buildUrlContext(일반 챗 + fetch_url 액션 공용) → ① 브릿지 추출 → ② fetchUrlDirect 폴백.
  • 비대상: bridgeFetch(의도적 localhost:3002 도구 호출)는 SSRF 가드 미적용 — 별도 함수.

🔁 재현 절차 (Reproduction)

  1. 챗 또는 모델 응답에 http://169.254.169.254/latest/meta-data/ 같은 내부 URL 포함.
  2. 기존 fetchUrlDirect: http/https만 확인하고 목적지 IP 미검증 → 그대로 요청, 본문 반환(webFetch.ts).
  3. 공인 도메인 → 302 리다이렉트로 사설 IP 유도 시 follow가 그대로 추적.

🔥 영향 및 심각도 (Impact & severity)

High. 내부망 스캐닝·클라우드 메타데이터 자격증명 탈취·로컬 전용 서비스(브릿지) 호출 가능. 입력 출처가 untrusted(웹/모델)라 악용 난도 낮음.

🧠 근본 원인 (Root cause)

  • fetchUrlDirect가 스킴(http/https)만 검사하고 호스트→IP 해석 후 사설/예약 범위 검증이 전무.
  • buildUrlContext가 URL을 브릿지·직접 양쪽으로 무검증 전달.
  • redirect:'follow'로 리다이렉트 목적지 재검증 불가.

🔎 조사 과정 (Investigation)

  • 보안 감사로 webFetch.ts:81-103·agent/actions/webFetch.ts:20의 무필터 fetch 경로 식별.
  • 외부 표준 리서치: OWASP는 denylist보다 allowlist(공인 IP 아니면 거부), DNS 리바인딩 방지엔 해석 시점 IP 검증/피닝 권고 [S5].

🛠️ 해결 (Resolution / applied fix)

  1. 신규 모듈 ssrfGuard.ts: isBlockedIPv4/IPv6/Ip, assertPublicUrl(url) — IP 리터럴 즉시 검사, DNS 호스트는 모든 A/AAAA 레코드 해석 후 하나라도 차단 대상이면 거부. localhost류는 해석 전 거부.
  2. urlContext.ts 입구에 assertPublicUrl — 실패 시 "🚫 URL 접근 차단" 정직 블록 반환(브릿지·직접 양 경로 차단).
  3. webFetch.ts fetchUrlDirect: redirect:'manual'로 전환, 최대 5홉 루프에서 각 홉 assertPublicUrl 재검증(이중 방어 + 독립 호출자 보호).

💻 코드 패턴 (Code patterns)

// ssrfGuard.ts — "공인 아니면 거부"
export async function assertPublicUrl(rawUrl: string): Promise<void> {
  const host = new URL(rawUrl).hostname.replace(/^\[|\]$/g, '');
  if (isIP(host)) { if (isBlockedIp(host)) throw new SsrfBlockedError(`차단된 내부 IP: ${host}`); return; }
  if (/^localhost$/i.test(host) || host.endsWith('.localhost')) throw new SsrfBlockedError('로컬 호스트');
  const addrs = await dnsLookup(host, { all: true });          // 모든 레코드 검사(다중 IP 우회 방지)
  for (const { address } of addrs)
    if (isBlockedIp(address)) throw new SsrfBlockedError(`내부 IP 해석: ${host}${address}`);
}
// webFetch.ts — 리다이렉트 각 홉 재검증
for (let hop = 0; hop <= 5; hop++) {
  try { await assertPublicUrl(current); } catch (e) { return fail(`차단됨(SSRF 방지): ${e.message}`); }
  res = await fetch(current, { redirect: 'manual', /* … */ });
  if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
    current = new URL(res.headers.get('location'), current).toString(); continue;
  }
  break;
}

검증 (Verification)

  • 신규 tests/ssrfGuard.test.ts 9개: IPv4/IPv6 범위, 내부 IP URL 거부, localhost 거부, 잘못된 URL 거부.
  • urlContextBuild.test.ts는 SSRF 가드를 모킹(할루시네이션 로직 검증 전용, DNS 비의존화).
  • tsc 0 에러, 전체 716 통과.

⚖️ 모순 및 업데이트 (Contradictions & updates)

한계(정직 고지): 해석 시점 검증이라 fetch 가 재해석하는 사이 레코드가 바뀌는 DNS 리바인딩 창은 완전히 닫지 못한다. 완전 차단은 연결 IP 고정(커스텀 undici dispatcher)이 필요 — 정적 사설 IP·리다이렉트 우회는 차단됨. 브릿지(:3002) 자체의 서버사이드 fetch SSRF는 브릿지(별도 프로젝트)의 책임.

🛠️ 적용 사례 (Applied in summary)

ASTRA 일반 챗 URL 주입 + 에이전트 fetch_url 액션 + /wikify 폴백(직접 fetch) 경로에 적용.

검증 상태 및 신뢰도

  • 상태: applied (미커밋)
  • 검증 단계: validated (단위 테스트 + 빌드/타입)
  • 출처 신뢰도: S
  • 신뢰 점수: 0.95
  • 중복 검사 결과: 신규 생성

🔗 지식 그래프 (Knowledge Graph)

📚 출처 (Sources)

  • [S1] E:/Wiki/astraai/src/features/web/ssrfGuard.ts — IP 범위 판정 + assertPublicUrl.
  • [S2] E:/Wiki/astraai/src/features/web/webFetch.tsfetchUrlDirect 수동 리다이렉트 + 가드.
  • [S3] E:/Wiki/astraai/src/lib/contextBuilders/urlContext.ts — 입구 chokepoint.
  • [S4] E:/Wiki/astraai/tests/ssrfGuard.test.ts — 회귀 테스트.
  • [S5] OWASP SSRF Prevention / azu/request-filtering-agent, Wiz SSRF guide — allowlist·DNS 리바인딩 권고.

📝 변경 이력 (Change history)

  • 2026-06-19: 보안 감사 + 외부 표준 대조 후 SSRF 가드 신설 및 최초 문서화(Claude Opus 4.8).