Skip to content

SSRF 검사를 직접 가져오기 한 곳으로 정리 - #112

Merged
m-a-king merged 8 commits into
mainfrom
fix/111-ssrf-single-check
Oct 18, 2026
Merged

m-a-king merged 8 commits into
mainfrom
fix/111-ssrf-single-check

Conversation

@m-a-king

@m-a-king m-a-king commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

한눈에

사내망 주소 검사를 extractor 가 직접 여는 연결 한 곳으로 정리

  • 이전: 같은 사내망 주소 검사를 세 번 함
    • 직접 가져오기의 매 단계, 렌더 호출 전, 렌더 결과 확인
    • 렌더 결과 확인은 https 주소만 보고 http 주소는 건너뜀
  • 직접 가져오기의 매 단계 검사만 남김
    • 사내망 대역 목록은 infra 저장소의 파일 하나를 읽음
    • https 에서 http 로 넘어가는 단계도 따라가고 같은 검사를 거침
  • 렌더 경로의 사내망 판정은 renderer 가 브라우저의 실제 연결 IP 로 함
    • extractor 는 renderer 의 응답 필드를 받아 같은 실패 코드로 바꿈
  • 사내망과 무관한 실패에 붙던 SSRF 실패 코드를 바로잡음
  • 머지하면 extractor 가 자동 배포되고 서버 방화벽 규칙이 걸림
    • renderer 주소만 예외로 열림
    • infra PR 이 먼저 머지돼야 함

Situation

원인 추가

  • SSRF 검사를 직접 가져오기 한 곳으로 정리 #111
  • 추가
    • 렌더는 직접 가져오기가 실패한 뒤에만 탐. 렌더 호출 전 검사는 같은 URL 을 두 번 보는 셈
    • 렌더 결과 확인은 렌더와 다른 시점의 이름 해석이라, 브라우저가 실제로 간 IP 와 답이 다를 수 있음

Task

범위

Action

서비스별 SSRF 방어 (세 PR 합본)

시점 공통 extractor: 보내기 전에 막음 renderer: 보내기 전엔 못 막고 받은 뒤에 버림
서비스가 뜰 때 infra 의 내부 주소 목록을 읽음. 파일이 없으면 뜨지 않음 차이 없음 차이 없음
HTTP 클라이언트·브라우저를 띄울 때 검사를 거치지 않는 요청 경로를 끔 HTTP 클라이언트의 자동 redirect 브라우저의 service worker·SharedWorker
주소를 처음 받을 때 https 이고 host 가 있는지 확인. 아니면 받자마자 거절 차이 없음 차이 없음
redirect 를 따라갈 때 http·https 만 따라감 Java 로 열 수 없는 주소(host 가 없거나 host 에 _ 가 있음)는 렌더로 넘김 차이 없음
IP 를 검사할 때 실제로 연결하는 IP 가 내부 주소인지 요청마다 검사 요청을 보내기 전. 검사한 IP 로만 연결 응답을 받은 뒤. 보내기 전에는 브라우저가 IP 를 직접 정해서 끼어들 수 없음. 평문 http 응답만 해당
연결을 열 때 (서버 방화벽) 내부 주소로 가는 연결을 거절. 예외는 DNS·메타데이터·Alloy renderer 주소도 예외 차이 없음
내부 주소로 나왔을 때 남은 요청을 보내지 않음. 결과를 버리고 BLOCKED_HOST 로 실패 확정 차이 없음 차이 없음
renderer 가 결과를 돌려줄 때 renderer 는 내부 주소를 만났는지를 internal_address_hit 에 참·거짓으로 늘 담아 보냄. 참이면 extractor 가 BLOCKED_HOST 로 실패 확정. 값이 아예 없으면 검사하지 않는 옛 renderer 의 결과로 보고 쓰지 않음 (HEADLESS_UPSTREAM, 일시 실패) 차이 없음 차이 없음
  • 두 서비스가 같이 하는 일은 공통 칸에 한 번. 서비스 칸에는 묶을 수 없는 차이만
  • renderer 앱은 요청이 나가기 전에 막지 못함
    • 원인: 이름을 IP 로 바꾸고 연결하는 일을 브라우저가 직접 함. 앱이 그 사이에 끼어들 자리 없음
    • 대안: 앱이 따로 이름을 IP 로 풀어 미리 검사
      • 기각: 브라우저가 받는 DNS 응답과 다를 수 있음. 막은 것처럼 보이기만 함. 지운 URL 글자 검사와 같은 이유
    • 대안: 브라우저의 모든 연결을 컨테이너 안 프록시로 보내고, 프록시가 검사한 IP 로만 연결
      • 기각: 프록시를 거치지 않는 연결(WebRTC)이 남음. 브라우저의 모든 연결이 새 구성 요소를 거침

목적지별로 누가 막나 (네트워크 목적지)

목적지 서버 방화벽 extractor: 보내기 전에 막음 renderer: 보내기 전엔 못 막고 받은 뒤에 버림
VPC 의 다른 서버 (DB·앱 서버) 거절 (즉시 RST) 보내기 전에 막음. BLOCKED_HOST 방화벽이 거절해 응답 없음. 페이지 이동이었으면 렌더 실패 (일시)
상대 서비스 주소 extractor 서버는 renderer 주소만 열어 둠. renderer 서버는 extractor 주소를 거절 보내기 전에 막음. BLOCKED_HOST. renderer 주소도 내부 주소 방화벽이 거절해 응답 없음. 페이지 이동이었으면 렌더 실패 (일시)
서버 자신 (sshd·nginx) 거절. 컨테이너에서 서버로 오는 연결은 4318 만 받음 보내기 전에 막음. BLOCKED_HOST 방화벽이 거절해 응답 없음. 페이지 이동이었으면 렌더 실패 (일시)
메타데이터 80·Alloy 4318 열어 둠 보내기 전에 막음. BLOCKED_HOST 요청은 닿음. 받은 뒤 결과를 버림. BLOCKED_HOST
컨테이너 자기 자신 (127.0.0.1·127.1·2130706433) 거치지 않음 보내기 전에 막음. BLOCKED_HOST. Java 도 변형 표기를 127.0.0.1 로 풂 (실측) 요청은 닿음. 받은 뒤 결과를 버림. BLOCKED_HOST. 브라우저도 127.0.0.1 로 알려 줌 (실측)
같은 bridge 의 옆 컨테이너 (같은 서비스의 다른 슬롯) 거치지 않음 (두 서버 모두 br_netfilter 꺼짐) 보내기 전에 막음. BLOCKED_HOST 요청은 닿음. 받은 뒤 결과를 버림. BLOCKED_HOST
검사와 연결 사이에 DNS 응답이 바뀜 VPC 대역이면 거절 빈틈 없음. 검사한 IP 로만 연결 빈틈 없음. 실제로 연결한 IP 를 검사

목적지별로 누가 막나 (요청 경로)

경로 서버 방화벽 extractor: 보내기 전에 막음 renderer: 보내기 전엔 못 막고 받은 뒤에 버림
https 에서 http 로 가는 redirect 해당 없음 따라감. 보내기 전에 IP 검사 따라감. 받은 뒤 IP 검사
https 응답 내부 주소면 거절 보내기 전에 IP 검사 IP 검사 안 함. 내부 서버는 그 이름의 공개 인증서가 없어 응답할 수 없음
https 아닌 입력 (http·file)·host 없는 입력 해당 없음 받자마자 거절. INVALID_URL 받자마자 거절. 400
service worker·SharedWorker 거절 해당 없음. 브라우저를 쓰지 않음 브라우저에서 꺼서 요청 0건
WebRTC·WebSocket 거절 해당 없음. 브라우저를 쓰지 않음 검사하지 못함. 브라우저가 이 연결의 IP 를 알려 주지 않음. 컨테이너 자기 포트에는 WebSocket 을 받는 곳이 없음
새 창 (window.open·target=_blank) 거절 해당 없음. 브라우저를 쓰지 않음 팝업 차단으로 열리지 않음 (실측). 열려도 같은 검사를 받음
연결 IP 를 알아내지 못함 해당 없음 해당 없음. 보내기 전에 IP 를 정함 내부 주소로 봄 (결과 페이지를 가져간 뒤 시작된 조회는 제외). BLOCKED_HOST
검사하지 않는 옛 renderer 의 응답 (배포 순서가 어긋남·롤백) 해당 없음 그 결과를 쓰지 않음. HEADLESS_UPSTREAM 일시 실패 해당 없음

내부 대역 정본 소비

  • 빌드가 shared-infra/contracts/internal-ranges.txt 를 classpath 에 실음 (proto 와 같은 자리)
  • 가드 생성 시점에 읽음. 파일이 빠진 빌드는 기동에서 실패
  • IPv4 를 담은 IPv6 는 JDK 가 Inet4Address 로 돌려줘 따로 풀지 않음 (테스트로 확인)
  • 127.1·2130706433 같은 변형 표기도 JDK 가 127.0.0.1 로 풀어 판정과 연결이 같은 IP 를 봄 (JDK 25 실측)
대역 예시 이전 이후
0.0.0.1·240.0.0.1 통과 차단
198.18.0.1 통과 통과
사설·loopback·링크 로컬·CGNAT·ULA 차단 차단

렌더 경로

  • 렌더 호출 전 재검사와 홉 재검사 삭제
  • renderer 가 internal_address_hit 를 참으로 보내면 BLOCKED_HOST 확정 실패
  • internal_address_hit 가 없는 응답(판정하지 않는 renderer)은 홉을 쓰지 않고 HEADLESS_UPSTREAM 일시 실패
    • 원인: 홉 재검사를 지웠으므로, renderer 보다 먼저 배포되거나 renderer 가 롤백되면 판정 없는 홉이 들어옴 (독립 리뷰 지적)
    • 결과: 머지 순서를 코드가 지킴. 순서가 어긋나면 렌더만 일시 실패
  • 홉 URL 은 baseUri 용 형식 판정만. 형식이 어긋난 홉은 원본 link 로 폴백

매 홉 http 허용

  • redirect 위치와 렌더 홉 주소가 http·https 를 받음
    • 입력 링크는 그대로 https 만
  • 이전: https 아닌 redirect 는 따라가지 않고 렌더로 승격
    • renderer 는 같은 http 홉을 따라감
    • 결과: 시스템 전체로는 다운그레이드 차단 효과 없음. 비대칭과 렌더 비용만 남음
    • 원인: 입력 링크 형식(https 만)을 redirect 위치 검사에 그대로 재사용한 잔재. SSRF 판정과 무관
  • 내부 주소 판정과 IP 고정은 scheme 과 무관하게 매 홉에 걸림
  • MALFORMED_REDIRECT 는 http·https 아닌 scheme 과 host 없는 redirect 위치만
선택지 매 홉의 http 비용
둘 다 허용하고 IP 판정 (채택) extractor·renderer 모두 따라감 평문 구간 변조 위험. renderer 가 이미 안고 있던 위험과 동일
이전 그대로 extractor 는 렌더로 승격, renderer 는 허용 비대칭. http redirect 는 늘 렌더 경로
renderer 도 http 이동 차단 둘 다 거절 http 로 넘어가는 몰의 렌더 실패. 하위 요청까지 막을지 기준 모호

host 존재 검사

  • 링크 형식 검사로 옮김. 입구와 매 redirect 가 같은 형식 검사를 거침
    • 둘의 차이는 scheme 기준뿐
  • 내부 주소 판정은 대역 판정만 맡음
  • 이전: 내부 주소 판정 안에서 host 를 검사함
    • 결과: host 없는 redirect 위치가 다음 홉에서 걸려 사용자 입력 오류(INVALID_URL)로 분류됨
  • redirect 위치의 host 검사는 SSRF 판단이 아님. Java 로 열 수 없는 주소를 렌더로 넘기는 길
    • http:///p·https:p·host 에 _ 가 든 주소는 Java 가 host 를 읽지 못함 (JDK 25 실측). 브라우저는 따라감
    • 상대 주소(/p·//host/p)는 현재 주소 기준으로 host 가 채워져 해당 없음
    • 대안: host 검사 없이 내부 주소 검사로 넘김
      • 기각: InetAddress.getAllByName(null) 이 127.0.0.1 을 돌려줌 (실측). 멀쩡한 외부 주소가 BLOCKED_HOST 확정 실패로 잘못 분류됨

실패 code

경우 이전 이후
http redirect BLOCKED_HOST, 승격 없음 따라감. 내부 IP 면 BLOCKED_HOST
http·https 아닌 redirect BLOCKED_HOST, 승격 없음 MALFORMED_REDIRECT, 헤드리스로 승격
host 없는 입력 주소 BLOCKED_HOST INVALID_URL
host 없는 redirect 위치 BLOCKED_HOST MALFORMED_REDIRECT, 헤드리스로 승격
내부 대역으로 resolve BLOCKED_HOST BLOCKED_HOST
렌더가 내부 주소에서 응답 받음 홉 URL 재해석으로 https 홉만 BLOCKED_HOST BLOCKED_HOST

배포

  • 새 슬롯을 띄우기 전에 infra 서버 벽 블록을 부름
  • renderer 주소는 배포가 매번 계산하는 값을 그대로 예외로 넘김

문서

  • docs/style-decisions.md 의 "헤드리스 경로의 redirect SSRF 비대칭" 항목을 해소로 갱신
    • ProductLink.HTTP_SCHEMES 잔가지는 입력·홉 scheme 집합으로 나뉘어 소멸로 표시
  • docs/api-contract.md 에 render.proto·internal-ranges.txt 행 추가

Result

검증

  • 정본에서 100.64.0.0/10 을 빼면 가드 테스트 2건(100.64.0.1·100.100.100.200)이 실패함. 가드가 정본을 실제로 읽음
  • 판정 필드 확인을 끄면 새 테스트 1건이 실패함
  • host 검사를 빼면 새 테스트 2건(입력 주소·redirect 위치)이 실패함
  • 홉도 https 만 받게 되돌리면 새 테스트 4건이 실패함
    • http redirect 따라가기·http redirect 의 내부 IP 차단·렌더 http 홉 유지·홉 형식
  • 빌드한 JAR 에 contracts/internal-ranges.txt 가 실림
  • 위험: CI 는 infra PR 머지 전까지 실패함. 내부 대역 정본과 렌더 계약 필드가 infra main 에 없음
  • 위험: http·https 아닌 redirect 가 이제 헤드리스로 넘어감. 그 렌더의 SSRF 는 renderer 판정과 서버 벽이 맡음
    • 판정하지 않는 renderer 의 홉은 쓰지 않음 (위 필드 확인)
  • 위험: 응답의 최종 주소(finalUrl)가 http 일 수 있음
    • core 는 파싱 귀결점의 http 주소 허용 core#1239 부터 http finalUrl 도 받아 https 와 같은 정체성으로 확정함
    • 그 PR 이 prod 에 오르기 전에는 http finalUrl 의 정체성 확정을 건너뜀 (경고 로그·CANONICAL_UNPARSABLE 메트릭)
    • 이전: 렌더 경로의 http 홉은 원본 링크로 폴백해 원본 링크로 정체성이 확정됨
    • 이전: 직접 가져오기의 http redirect 는 BLOCKED_HOST 확정 실패

후속


연관 이슈

- 내부 대역 판정을 infra contracts/internal-ranges.txt 로 바꿈
  - 빌드가 정본을 classpath 에 실음. 없으면 가드를 만드는 빈 생성에서 기동 실패
  - 이전: JDK 주소 분류 + CGNAT·ULA·메타데이터 개별 판정
    - 결과: renderer 판정과 6개 대역에서 어긋남
- 렌더 호출 전의 첫 URL 재검사 삭제
  - 원인: 렌더는 직접 가져오기가 실패한 뒤에만 탐. 같은 URL 이 이미 매 홉 검사를 통과함
- 렌더 홉 URL 의 재검사 삭제
  - 이전: https 홉만 다시 이름 해석해 검사, http 홉은 건너뜀
  - 원인: 렌더와 다른 시점의 이름 해석이라 브라우저가 실제로 간 IP 와 어긋날 수 있음
  - renderer 가 실제 연결 IP 로 판정해 internal_address_hit 로 알리면 BLOCKED_HOST 로 번역
- https 아닌 redirect 를 MALFORMED_REDIRECT 로 내고 헤드리스로 승격
  - 이전: BLOCKED_HOST. 내부 주소가 아닌데 SSRF 로 집계되고 승격도 막힘
- host 없는 주소는 INVALID_URL
  - 이전: BLOCKED_HOST
- 판정을 끄는 대조: 정본에서 100.64.0.0/10 을 빼면 가드 테스트 2건 실패
- 배포가 infra 의 egress_wall.sh 로 서버 벽을 건 뒤 새 슬롯을 띄움
- 이 서비스 고유 예외는 renderer 주소 하나
  - 배포가 매번 계산하는 renderer 주소를 그대로 넘김. 포트·박스가 바뀌어도 같이 따라감
@m-a-king m-a-king added the fix 외부 가시적 결함 수정 label Oct 14, 2026
@m-a-king m-a-king linked an issue Oct 14, 2026 that may be closed by this pull request
@m-a-king m-a-king self-assigned this Oct 14, 2026
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e923896e-7750-427b-9fd1-d872f4ae355c
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

- 렌더 응답에 internal_address_hit 가 없으면 홉을 쓰지 않고 HEADLESS_UPSTREAM 일시 실패
  - 원인: extractor 가 홉 재검사를 지웠음. renderer 가 판정 전 버전이면(배포 순서가 어긋나거나 롤백) 내부 응답이 그대로 들어옴 (독립 리뷰 지적)
  - 계약이 이 필드를 optional 로 바꿔 판정 여부를 실음 (TeamPiKi/infra#138)
  - 결과: 머지 순서를 코드가 지킴. renderer 가 먼저면 안전, extractor 가 먼저면 렌더만 일시 실패
- 음성 대조: 확인을 끄면 새 테스트 1건 실패
m-a-king added a commit that referenced this pull request Oct 15, 2026
- 링크 추출 요청마다 기한을 ScopedValue 로 묶음
  - 기한: 계약의 호출자 read 타임아웃(55초)에서 여유 5초를 뺀 50초
  - 이미지 경로·단위 테스트는 기한이 묶이지 않아 그대로
- 기한이 지나면 헤드리스 렌더·LLM 단계를 새로 시작하지 않음
  - 원인: core 는 55초에 끊고 재요청함. 그 뒤 시작한 단계는 받을 곳이 없고 재요청과 renderer 단일 슬롯·LLM 비용을 나눠 씀
  - 실패는 일시 실패 UPSTREAM_ERROR(502)
  - 진행 중인 단계는 끊지 않음
  - 대안: 남은 시간이 렌더 예산(22초)보다 적으면 렌더를 시작하지 않음
    - 기각: LLM 뒤 필드 부족으로 렌더로 넘어가는 경로가 지금은 55초 안에 성공하는데 실패로 바뀜
  - 대안: core 가 남은 기한을 헤더로 보냄
    - 기각: core 의 read 타임아웃이 고정이라 헤더가 새로 주는 정보 없음. 배포가 세 repo 로 늘어남
- redirect 홉 사이에는 검사하지 않음
  - 원인: 홉은 renderer·LLM 을 쓰지 않음. 열려 있는 SSRF 정리 PR(#112)과 같은 파일
m-a-king added a commit that referenced this pull request Oct 15, 2026
- SiteConcurrencyLimiter 추가. 정적 fetch 를 사이트별 세마포어로 감쌈
  - 사이트는 공개 접미사 목록 기준 등록 도메인(guava InternetDomainName). www·m 이 한 사이트
  - 기본값: 사이트당 2개, 최대 대기 15초(fetch.site-limit.*)
  - 원인: 몰 요청이 고정 IP 하나에서 나가 몰의 IP 단위 제한에 걸리면 그 몰 전체가 막힘
- 자리를 못 얻으면 SiteBusyException(UPSTREAM_ERROR 일시 실패)
  - PageFetchException 이 아니라서 헤드리스로 넘어가지 않음
  - 원인: 넘기면 같은 사이트를 렌더 서버 IP 로 다시 요청해 상한이 무력해짐
- 제한 위치는 DefaultProductLinkExtractor 의 fetch 호출
  - 대안: PageFetcher 를 감싸는 @primary 빈
    - 기각: 통합 테스트의 @primary StubPageFetcher 와 충돌
  - 대안: HttpPageFetcher 의 redirect 홉마다 제한
    - 기각: 열려 있는 SSRF 정리 PR(#112)과 같은 파일. 홉 사이 사이트가 바뀌는 체인은 짧음
- 줄무늬 1024 개의 약한 참조 세마포어
  - 원인: 사이트 키가 늘어도 메모리가 자라지 않음
- ProductLink.parse 가 host 없는 주소를 INVALID_URL 로 거절
  - 입구와 매 redirect 가 같은 함수를 거쳐 두 곳 모두 검사
- InternalHostGuard 의 host 없음 분기 삭제. 가드는 내부 대역 판정만 맡음
- host 없는 redirect 위치를 MALFORMED_REDIRECT 로 바꿈
  - 이전: 다음 홉의 가드에서 걸려 INVALID_URL. 사용자 입력 오류로 잘못 분류됨
- 음성 대조: host 검사를 빼면 새 테스트 2건 실패
m-a-king added a commit that referenced this pull request Oct 15, 2026
- 링크 추출 요청마다 기한을 ScopedValue 로 묶음
  - 기한: 계약의 호출자 read 타임아웃(55초)에서 여유 5초를 뺀 50초
  - 이미지 경로·단위 테스트는 기한이 묶이지 않아 그대로
- 기한이 지나면 헤드리스 렌더·LLM 단계를 새로 시작하지 않음
  - 원인: core 는 55초에 끊고 재요청함. 그 뒤 시작한 단계는 받을 곳이 없고 재요청과 renderer 단일 슬롯·LLM 비용을 나눠 씀
  - 실패는 일시 실패 UPSTREAM_ERROR(502)
  - 진행 중인 단계는 끊지 않음
  - 대안: 남은 시간이 렌더 예산(22초)보다 적으면 렌더를 시작하지 않음
    - 기각: LLM 뒤 필드 부족으로 렌더로 넘어가는 경로가 지금은 55초 안에 성공하는데 실패로 바뀜
  - 대안: core 가 남은 기한을 헤더로 보냄
    - 기각: core 의 read 타임아웃이 고정이라 헤더가 새로 주는 정보 없음. 배포가 세 repo 로 늘어남
- redirect 홉 사이에는 검사하지 않음
  - 원인: 홉은 renderer·LLM 을 쓰지 않음. 열려 있는 SSRF 정리 PR(#112)과 같은 파일
m-a-king added a commit that referenced this pull request Oct 15, 2026
- SiteConcurrencyLimiter 추가. 정적 fetch 를 사이트별 세마포어로 감쌈
  - 사이트는 공개 접미사 목록 기준 등록 도메인(guava InternetDomainName). www·m 이 한 사이트
  - 기본값: 사이트당 2개, 최대 대기 15초(fetch.site-limit.*)
  - 원인: 몰 요청이 고정 IP 하나에서 나가 몰의 IP 단위 제한에 걸리면 그 몰 전체가 막힘
- 자리를 못 얻으면 SiteBusyException(UPSTREAM_ERROR 일시 실패)
  - PageFetchException 이 아니라서 헤드리스로 넘어가지 않음
  - 원인: 넘기면 같은 사이트를 렌더 서버 IP 로 다시 요청해 상한이 무력해짐
- 제한 위치는 DefaultProductLinkExtractor 의 fetch 호출
  - 대안: PageFetcher 를 감싸는 @primary 빈
    - 기각: 통합 테스트의 @primary StubPageFetcher 와 충돌
  - 대안: HttpPageFetcher 의 redirect 홉마다 제한
    - 기각: 열려 있는 SSRF 정리 PR(#112)과 같은 파일. 홉 사이 사이트가 바뀌는 체인은 짧음
- 줄무늬 1024 개의 약한 참조 세마포어
  - 원인: 사이트 키가 늘어도 메모리가 자라지 않음
- redirect 위치와 렌더 홉 주소가 http 도 받음
  - 입력 링크는 그대로 https 만
  - 원인: 내부 주소 판정과 IP 고정은 scheme 과 무관하게 매 홉에 걸림
  - 원인: http redirect 를 거절해도 헤드리스로 넘어가 renderer 가 같은 홉을 따라감. 다운그레이드 차단 효과 없음
  - 결과: 매 홉의 scheme 기준이 renderer 와 같아짐
- MALFORMED_REDIRECT 는 http·https 아닌 redirect 와 host 없는 redirect 위치만
- 렌더의 http 홉을 원본 link 로 폴백하지 않고 홉 주소로 씀
  - finalUrl 이 http 일 수 있음
- 음성 대조: 홉도 https 만 받게 되돌리면 새 테스트 4건 실패
- style-decisions 의 헤드리스 redirect SSRF 비대칭 항목을 해소로 갱신
  - final_url 사후 검증 삭제 이유
  - 직접 가져오기는 보내기 전 IP 검사와 IP 고정
  - renderer 는 보내기 전에 막지 못해 받은 뒤 결과를 버림
- style-decisions 의 ProductLink.HTTP_SCHEMES 잔가지를 소멸로 표시
- api-contract 에 render.proto·internal-ranges.txt 행 추가
@m-a-king
m-a-king merged commit ee38577 into main Oct 18, 2026
3 checks passed
@m-a-king
m-a-king deleted the fix/111-ssrf-single-check branch October 18, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix 외부 가시적 결함 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSRF 검사를 직접 가져오기 한 곳으로 정리

1 participant