CVE-2026-33953

CVE-2026-33953

Area
Web / AppSec
Credit
Public Attribution
Credit Note
CVSS
8.5
CVSS Source
CNA / Vendor
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
CWE
CWE-918
Impact
SSRF
Key Finding
사설 IP literal은 차단했지만 internal hostname의 DNS 해석 결과는 검사하지 않아 내부 서비스 요청이 가능했습니다.
Published
Mar 27, 2026
Score Note
Severity
High
Status
Public
Target
LinkAce · URL fetch / SSRF guard
사설 IP 문자열은 거부됐지만 같은 내부 서비스의 Docker 호스트명을 넣으면 검사를 통과했습니다. 입력 문자열 검사와 실제 DNS 해석 목적지를 분리해 확인했습니다.
Taegu Ha · LinkAce · CWE-918 · CVSS 8.5
2026년 3월 27일 공개 · LinkAce 2.5.3에서 수정 · 공식 기록

한눈에 보기

🔎
LinkAce의 private-IP validation은 입력이 IP literal이 아니면 검사를 끝냈습니다. 이후 HTTP client가 hostname을 private IP로 해석해 내부 서비스에 요청할 수 있었습니다.
구분
확인 내용
프로젝트·컴포넌트
LinkAce URL validation · NoPrivateIpRule
공격 입력
private IP로 해석되는 내부 hostname
필요 조건
URL을 등록·가져올 수 있는 authenticated user
취약 지점
hostname의 A·AAAA resolution 전에 IP literal 여부만 검사
검증 결과
검사를 통과한 hostname이 내부 address로 resolve되어 internal service fetch
영향
SSRF, 내부 서비스·metadata 접근 가능성
수정 원칙
연결 전과 redirect마다 resolution 결과의 모든 address에 public-IP policy 적용

공격 흐름

검증 결론: validation 시점의 문자열 분류와 실제 연결 시점의 DNS resolution이 분리되어 hostname 기반 private-network SSRF가 성립함을 확인했습니다.

취약점 개요

LinkAce는 링크의 제목과 메타데이터를 가져오기 위해 사용자가 입력한 URL로 서버 측 요청을 보냅니다. NoPrivateIpRule은 URL host가 IP literal일 때 private/reserved range를 차단했지만, host가 이름이면 IP 검사를 하지 않고 통과시켰습니다.
Docker 네트워크 안에만 존재하고 host port를 공개하지 않은 HTTP 서비스를 만들었습니다. 그 서비스의 사설 IP를 URL에 직접 넣으면 거부됐지만 Docker DNS 이름을 넣으면 LinkAce가 내부 주소로 해석해 내용을 가져왔습니다.
공격자가 제어하는 값
저장·가져오기 기능에 입력하는 URL
검사 지점
NoPrivateIpRule::validate()
실제 요청
LinkAce 서버의 DNS resolver와 HTTP client
영향
서버가 접근 가능한 내부 HTTP 서비스 요청

조사 대상

SSRF 방어를 검토할 때 URL parser가 보는 host 문자열과 socket이 연결하는 최종 IP를 구분했습니다. redirect, DNS, 여러 A/AAAA 응답처럼 두 시점 사이에 값이 바뀌는 경우가 있기 때문입니다.
이 구현은 FILTER_VALIDATE_IP가 false이면 ‘hostname is not an IP address’라는 이유로 즉시 return했습니다. 이름을 실제로 resolve한 뒤 private range를 다시 확인하는 단계가 없었습니다.
사설 IP literal 차단을 통과한 내부 hostname이 실제로 같은 사설 서비스까지 요청되는가?

원인 분석

검사는 원본 URL의 host 문자열만 봤습니다. 172.18.0.7은 IP이므로 private range 필터에 걸리지만 ssrf-test는 IP가 아니어서 통과합니다. 이후 HTTP client는 ssrf-test를 Docker DNS에서 172.18.0.7로 해석했습니다.
validation과 fetch 사이의 의미가 달랐습니다. validator는 ‘입력이 IP인가’를 보았고 보안 정책은 ‘최종 목적지가 public address인가’를 보장해야 했습니다. 두 값이 우연히 같은 IP literal일 때만 방어가 동작했습니다.
NoPrivateIpRule.php · hostname을 즉시 허용한 분기
$domain = parse_url($value, PHP_URL_HOST); if (filter_var($domain, FILTER_VALIDATE_IP) === false) { // Hostname is not an IP address return; } if (is_private_or_reserved($domain)) { $fail(...); }
입력에서 영향까지의 경로
  1. 인증 사용자가 내부 host 이름이 든 URL을 제출합니다.
  1. validator는 host가 IP literal이 아니라는 이유로 허용합니다.
  1. LinkAce worker가 URL의 메타데이터를 가져옵니다.
  1. 서버 DNS가 이름을 private IP로 해석합니다.
  1. 외부 사용자가 직접 닿지 못하는 서비스에 요청이 도착합니다.

재현 및 검증

LinkAce와 ssrf-test를 같은 Docker network에 두고 test service에는 host port를 publish하지 않았습니다. 응답 본문에 식별 가능한 marker를 넣고 요청 로그를 켰습니다.
IP literal, hostname, public URL을 나눠 입력했습니다. 내부 IP 차단 메시지와 내부 hostname fetch 성공, service 측 User-Agent 로그를 함께 남겨 단순 validation UI 오동작이 아니라 실제 server-side request임을 확인했습니다.
$ submit-link http://172.18.0.7/ $ submit-link http://ssrf-test/ $ docker logs ssrf-test
검사
입력·조건
관찰
의미
IP 차단
http://172.18.0.7
private IP 오류
기존 방어가 활성화됨
Hostname 우회
http://ssrf-test
메타데이터 fetch 성공
이름 입력은 검사 통과
네트워크 대조
host에서 service port 접근
published port 없음
외부 직접 접근과 분리
서버 로그
LinkAce fetch 후 로그
LinkAce User-Agent 요청
실제 server-side 연결
두 표현으로 같은 내부 목적지를 지정한 결과
URL: http://172.18.0.7 result: rejected — must not contain a private IP URL: http://ssrf-test resolved by LinkAce network: 172.18.0.7 result: fetched internal marker service log: GET / ... LinkAce user-agent

판단 변화

Validation 우회

처음에는 hostname이 form validation을 통과한다는 사실만 확인했습니다. 하지만 이후 fetch가 실패한다면 보안 영향은 없을 수 있습니다. 내부 service 로그와 가져온 marker를 추가해 validation bypass와 SSRF를 구분했습니다.

외부 접근

localhost에 단순 서버를 띄우면 공격자도 같은 포트에 접근할 수 있어 SSRF의 네트워크 경계를 설명하기 어렵습니다. host port를 publish하지 않은 Docker service를 사용해 LinkAce 컨테이너만 접근 가능한 목적지를 만들었습니다.

Redirect 범위

DNS hostname만으로 재현을 완료했으므로 redirect chain과 DNS rebinding까지 모두 된다고 주장하지 않았습니다. 수정에서는 resolve 후 검사와 redirect마다 재검사를 고려해야 하지만, 확인한 공격 형태는 내부 hostname resolution입니다.

영향과 수정

링크를 제출할 수 있는 인증 사용자는 LinkAce 서버가 접근할 수 있는 내부 HTTP 서비스로 요청을 보내고 응답 메타데이터를 관찰할 수 있었습니다. 내부 관리 서비스 탐색과 제한적 데이터 노출로 이어질 수 있습니다.

확인한 범위

  • 사설 IP literal 거부
  • 같은 내부 목적지의 hostname fetch 성공
  • 내부 service의 실제 요청 수신

제외한 범위

  • 임의 TCP 프로토콜
  • cloud metadata 탈취의 실환경 확인
  • DNS rebinding과 redirect 우회

수정

2.5.3에서는 hostname을 resolve한 최종 주소에도 public-address 정책을 적용하도록 수정됐습니다. SSRF 필터는 URL 문자열뿐 아니라 연결 직전의 모든 A/AAAA 주소와 redirect 목적지를 같은 정책으로 검사해야 합니다.

분석 기준과 증거 보존

항목
최종 감사 결과
소스 기준
LinkAce checkout 605ce0401036bb54408f043ebd3f8ee0366fa036
원본 검증
validation rule과 HTTP fetch 경로 비교, internal hostname 입력과 private resolution 관찰
GitHub 보존 상태
Public 저장소 main의 감사 commit c04cacf; evidence 5개와 root manifest.json에 size·SHA-256 고정
무결성
사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료
보존 경계·제약
실제 내부 자산 주소, cookie와 원본 session은 제외
감사 완료
2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료

관련 파일

2026-08-23 최종 감사

취약 validator와 fetch controller의 소스 스냅샷, 공개 advisory와 CVE 레코드를 포함했습니다.

느낀 점

입력값이 안전한지 묻는 것과 실제 연결 목적지가 안전한지 묻는 것은 달랐습니다. SSRF 방어는 parser 한 번으로 끝나는 검사가 아니라 DNS와 redirect를 지나 socket이 선택할 주소까지 이어지는 과정이라는 점이 가장 크게 남았습니다.
Docker의 내부 DNS와 미공개 port를 대조군으로 사용하자 ‘왜 이 요청이 SSRF인가’를 별도 설명 없이 보여줄 수 있었습니다. 재현 환경의 네트워크 구조 자체가 증거가 된 사례였습니다.