Skip to content

fix(net): block :: and fec0::/10, and start the parity audit (0.7.1) - #28

Merged
jlc488 merged 1 commit into
mainfrom
fix/ipv6-unspecified-and-site-local
Aug 8, 2026
Merged

fix(net): block :: and fec0::/10, and start the parity audit (0.7.1)#28
jlc488 merged 1 commit into
mainfrom
fix/ipv6-unspecified-and-site-local

Conversation

@jlc488

@jlc488 jlc488 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

⚠️ 머지하면 0.7.1이 npm에 배포됩니다. 보안 수정이라 의도한 바입니다.

로드맵 P3-1(JVM ↔ JS 정합성 정기 점검) 첫 회차. 양쪽에서 두 건씩, 총 4건이 나왔습니다 — 감사를 돌릴 값어치가 있었는지에 대한 답입니다.

이쪽(JS)이 느슨했던 것

주소 이전 JVM
:: (unspecified) 공인으로 분류 isAnyLocalAddress() → 차단
fec0::/10 (site-local) 공인으로 분류 isSiteLocalAddress() → 차단

::로 연결하면 로컬 호스트에 닿습니다 — 이미 차단하던 0.0.0.0의 IPv6 짝입니다. fec0::/10은 RFC 3879로 폐지돼서 빼먹기 쉽지만, 폐지 이전에 구축된 망에서는 여전히 라우팅되고 내부 서비스가 사는 곳이 바로 거깁니다.

둘 다 설계상의 선택이 아니라 그냥 없었습니다. 이 주소로 해석되는 호스트명이 blockPrivateNetworkssafeFetch의 DNS 검사를 통과했습니다. URL 시점 검사는 무관합니다(http://[::]/는 이미 rejectIpLiteralHosts가 거부).

추론이 아니라 실측입니다. 주소 목록을 빌드된 패키지에 넣어 돌리고 JVM 헬퍼의 답과 대조했습니다 — 두 구현을 나란히 읽는 건 애초에 0.1.2 버그가 리뷰를 통과한 방식이라서요. fe80::/10fec0::/10은 2비트 차이라, 마스크가 틀리면 fe00::/8 전체를 조용히 삼킵니다. 그래서 fe00::·fe40::이 공인으로 남는지도 테스트로 못 박았습니다.

자바가 느슨했던 것

툴 입력 스캐너가 http(s)://만 수집하고 나머지 스킴은 정책이 보기 전에 버렸습니다 — file://·gopher://·ftp://조용히 통과. 프로토콜 상대 //host는 수집조차 안 됐습니다.

0.1.2 우회와 정확히 같은 형태입니다 — 수집 단계 필터가 URL을 검증에서 거부되게 하는 대신 빠져나가게 함. ssrf-guard#20에서 미출시 3.2.0에 수정했습니다(./gradlew build SUCCESSFUL, 231 테스트 전부 통과).

docs/parity.md (+ 한국어 짝)

이게 P3-1의 진짜 산출물입니다:

  • 언제 훑나 — 어느 쪽이든 코어 로직(스캐너·IP 분류·리다이렉트·차단 사유)을 바꿀 때
  • 네 영역 체크리스트
  • 이번 감사 결과 — 해결 4건, 미해결 1건, 동등 확인 4건
  • 의도적으로 다른 것 — JS 전용 기능은 정합성 실패가 아님. 양쪽에 다 있는데 다르게 구현된 것이 실패

그리고 원칙 하나를 못 박았습니다: 소스가 아니라 동작을 대조할 것.

일부러 미해결로 남긴 것

자바의 리다이렉트 홉이 스킴 + 호스트/DNS만 재검증합니다. 포트·userinfo·IP-리터럴을 다시 안 봐서, 허용 호스트에서 https://allowed.example:9999/로 가는 리다이렉트를 따라갑니다(JS는 막음).

JS는 공유 루프 하나에서 홉마다 validateUrl 전체를 돌리지만, 자바는 리다이렉트 처리가 어댑터 6종에 흩어져 각자 자기 클라이언트 전략에 위임합니다. 패치가 아니라 전 어댑터 설계 변경이라 이번 범위 밖으로 두고 문서에 근거와 함께 기록했습니다.

검증

typecheck · 223 테스트/15파일(기존 219) · build · 런타임 체크 Node 그린.

The first JVM <-> JS parity audit (P3-1). It found four divergences —
two on each side — which is the answer to whether the audit was worth
running.

This side was looser in two IPv6 cases:

  - `::`, the unspecified address, was classified as public. Connecting
    to it reaches the local host; it is the IPv6 counterpart of 0.0.0.0,
    which was already blocked.
  - `fec0::/10`, IPv6 site-local. Deprecated by RFC 3879 and therefore
    easy to leave out, but still routed on networks that predate the
    deprecation — which is exactly where an internal service lives.

The Java sibling catches both through InetAddress.isAnyLocalAddress()
and isSiteLocalAddress(), so neither was a design choice here; they were
simply absent. A hostname resolving to either passed blockPrivateNetworks
and safeFetch's DNS checks. URL-time checks were unaffected: an
IP-literal URL like http://[::]/ was already rejected.

Measured, not inferred. The address list was run through the built
package and compared against what the JVM helpers return, because
reading two implementations side by side is exactly how the 0.1.2 bug
survived review. The fe80::/10 and fec0::/10 masks differ in two bits,
so tests also pin fe00:: and fe40:: as public — a wrong mask would
swallow the whole fe00::/8 block silently.

The Java side was looser in two others, both in the tool-input scanner
and both the same shape as the 0.1.2 bypass — a collection-stage filter
letting URLs skip validation rather than be rejected by it. Fixed in
ssrf-guard#20 against the unreleased 3.2.0.

Also adds docs/parity.md and its Korean twin: the checklist for the four
areas that must agree, this audit's findings in both directions, the one
divergence deliberately left open (the JVM re-validates only scheme and
host per redirect hop, spread across six adapters), and the list of
differences that are intended rather than drift.

Verified: typecheck, 223 tests in 15 files (was 219), build, and the
runtime check green on Node.
@jlc488
jlc488 merged commit 37778e6 into main Aug 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant