Nezha는 소유권 확인 없이 WebSocket 스트림 UUID를 통한 교차 테넌트 터미널/파일 관리자 세션 하이재킹에 취약합니다.
nezha v1.14.13–v1.14.14 및 v2.0.0–v2.0.9에서 WebSocket 엔드포인트 GET /ws/terminal/:id 및 GET /ws/file/:id는 유효한 스트림 UUID가 있어야만 호출자를 인증하며 해당 UUID를 스트림을 생성한 사용자에게 연결하는 소유권 확인은 없습니다. 라이브 스트림 UUID를 학습한 인증된 대시보드 사용자('RoleMember' 포함)는 세션에 연결하여 대상 서버에서 대화형 셸 액세스 또는 전체 파일 관리자 제어권(예: 교차 테넌트 RCE)을 얻을 수 있습니다.
이 문제는 커밋 6661d6a(2026-05-18, v2.0.10에서 제공됨)에서 자동으로 수정되었습니다. 제출 시점에는 이 수정 사항을 다루는 공개 CVE/GHSA가 없으므로 v1.14.x 및 v2.0.10 이전 v2.x 배포 운영자는 취약한 코드를 실행하고 있다는 신호를 받지 못합니다.
스트림 할당 — service/rpc/io_stream.go(v2.0.9):
``가다 func (s *NezhaHandler) CreateStream(streamId 문자열) { s.ioStreamMutex.Lock() s.ioStreamMutex.Unlock()을 연기합니다.
s.ioStreams[streamId] = &ioStreamContext{
userIoConnectCh: make(chan struct{}),
AgentIoConnectCh: make(chan struct{}),
}
}
어떤 제작자도 스트림에 묶여 있지 않습니다.
**스트림 연결 — `cmd/dashboard/controller/terminal.go`(v2.0.9):**
``가다
// @Router /ws/terminal/{id} [get]
func TerminalStream(c *gin.Context) (모든, 오류) {
streamId := c.Param("id")
_인 경우 err := rpc.NezhaHandlerSingleton.GetStream(streamId); 오류 != 없음 {
0을 반환, 오류
}
rpc.NezhaHandlerSingleton.CloseStream(streamId) 연기
// ... WebSocket 업그레이드 및 양방향 파이프 ...
}
유일한 인증 확인은 GetStream(streamId)입니다. 즉, "이 UUID가 메모리 내 맵에 존재합니까?"입니다. getUid(c)는 createTerminal을 호출한 사용자와 비교되지 않습니다. cmd/dashboard/controller/fm.go의 fmStream(c)에도 동일한 패턴이 있습니다.
UUID가 유출되는 곳:
createTerminal은 합법적인 클라이언트에 UUID를 반환하고, 그러면 wss://<dashboard>/ws/terminal/<UUID>가 열립니다. URL 경로 구성 요소로서 UUID는 다음을 통해 노출됩니다.
이러한 사이드 채널 중 하나에 액세스할 수 있는 인증된 사용자는 누구나 라이브 세션에 연결할 수 있습니다.
admin(RoleAdmin, 서버 소유) 및 member(RoleMember, 해당 서버에 액세스할 수 없음).admin으로 서버용 웹 터미널을 엽니다. 브라우저가 wss://<dashboard>/ws/terminal/<UUID>를 엽니다. 네트워크 검사기, 서버 액세스 로그 또는 'Referer' 헤더에서 이 UUID를 캡처하세요.member로 로그인된 별도의 세션에서 wss://<dashboard>/ws/terminal/<UUID>(동일한 UUID)를 엽니다. 멤버의 WebSocket은 동일한 ioStreamContext에 연결됩니다. 왜냐하면 terminalStream은 GetStream(streamId)만 확인하고 소유권은 확인하지 않기 때문입니다.동일한 흐름이 /ws/file/:id(파일 관리자 하이재킹: 대상 서버의 파일 시스템에서 임의 읽기/쓰기)에 대해 작동합니다.
/ws/file/:id는 대상 파일 시스템에서 임의의 읽기+쓰기를 노출합니다. /ws/terminal/:id는 전체 쉘입니다.이는 CVE-2026-46716(교차 테넌트 cron RCE)과 동일한 영향 계층이며 진입점이 인증된 POST가 아닌 수동적으로 유출된 URL이기 때문에 더 심각할 수 있습니다. 로그 또는 원격 측정을 통해 UUID가 유출되면 공격자는 직접적인 대시보드 상호 작용이 필요하지 않습니다.
커밋 6661d6a으로 마스터에서 이미 수정되었습니다("fix(rpc): 터미널/fm 하이재킹을 방지하기 위해 io_stream 세션을 생성자에 바인딩"):
IsStreamAuthorizedForUser(streamId, userID, isAdmin) 도우미.terminalStream 및 fmStream은 WebSocket 업그레이드 전과 CloseStream(streamId) 연기 전에 이 도우미를 호출하므로 거부된 시도가 합법적인 스트림을 중단하지 않습니다.v2.0.10(2026-05-19)으로 출시되었습니다. v1.14 라인은 백포트를 수신하지 못했습니다.
수정 사항은 조용히 착륙했습니다. 다른 5월 17~21일 수정 사항은 공개 GHSA(GHSA-99gv-2m7h-3hh9, GHSA-rxf6-wjh4-jfj6, GHSA-hvv7-hfrh-7gxj, GHSA-w4g9-mxgg-j532, GHSA-6x26-5727-rrm9, GHSA-4g6j-g789-rghm)은 cron RCE, AlertRule 트리거, 원격 측정 누출, 알림 SSRF, DDNS SSRF 및 에이전트 위조 결과를 각각 다루지만 터미널/파일 관리자 세션 하이재킹은 다루지 않습니다. 이 권고는 v1.14.x 및 v2.0.0–v2.0.9의 운영자가 업그레이드를 알 수 있도록 이러한 격차를 해소합니다.
6661d6a를 v1.14.15 릴리스로 백포트하거나 SECURITY.md에 v1.14 라인의 수명 종료를 표시하여 운영자가 지원 범위를 이해할 수 있도록 합니다.왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 99.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 99.00 |
VPI 공식 vpi-v1 기준