Skip to content

fix(realtime): 문서 SSE 채널이 소유권 검증 없이 열려 있던 문제 - #186

Merged
i3months merged 2 commits into
devfrom
fix/document-stream-scope
Aug 19, 2026
Merged

fix(realtime): 문서 SSE 채널이 소유권 검증 없이 열려 있던 문제#186
i3months merged 2 commits into
devfrom
fix/document-stream-scope

Conversation

@i3months

Copy link
Copy Markdown
Member

변경 사항

마지막 점검에서 RealTime(Go) 서버를 처음 읽다가 나왔다. 오늘 CI 에 들어오긴 했지만 코드를 본 적은 없던 축이다.

라우터의 채널 4개 중 document 만 토큰 리소스 범위를 확인하지 않고 있었다:

pr.Get("/realtime/stream/documents/{id}", channelByPath(sse, session.ChannelDocument)) // TODO: DOCUMENT 스코프 검증 (deferred)

세션 채널 3개(/realtime/stream/sessions/{id}, /realtime/sessions/{id}, .../audio)는 모두 이렇게 막고 있다:

if c.ResourceType != "SESSION" || c.ResourceID != id { 403 }

무엇이 새는가

이 채널로 흐르는 건 AnalysisCallbackPayload 다:

record AnalysisCallbackPayload(
    String targetType, Long targetId, String status,
    String summary,            // AI 가 만든 이력서/자소서 요약
    List<String> techStack,
    String documentPath,       // S3 경로
    ...
)

남의 이력서·자소서 분석 결과다. 유효한 스트림 토큰만 있으면(로그인한 사용자는 자기 것으로 언제든 발급 가능) document id 를 바꿔가며 타인의 분석 결과를 구독할 수 있었다. id 는 BIGSERIAL 이라 열거도 쉽다.

범위 제한 요소: 이벤트 replay 가 없어 구독 중에 완료되는 분석만 받는다. 다만 연결을 열어두면 계속 수확할 수 있다.

수정

세션 채널과 같은 방식으로 막는다. RealTime 은 DB 를 보지 않으므로, Core 가 소유권을 확인해 발급한 토큰의 리소스 범위를 강제하는 것이 유일한 소유권 검사다. channelByPathscopedChannel(sse, kind, resourceType) 로 일반화했다.

깨지는 사용처가 없는 이유

  • Core 는 현재 DOCUMENT 스코프 토큰을 발급하지 않는다(USER / SESSION 만)
  • 프론트도 이 채널을 쓰지 않는다 — 분석 상태는 /realtime/stream/me(유저 채널)로 받는다. AnalysisCallbackService 가 document·user 양쪽에 publish 하고, 코드 주석도 "프론트가 documentId 를 모르고도 /realtime/stream/me 로 받을 수 있게"라고 적고 있다

제품에서 안 쓰이면서 열려 있던 경로다. 나중에 DOCUMENT 토큰을 도입하면 이 검증이 그대로 동작한다.

테스트 (4)

  • 다른 리소스 타입(SESSION) 토큰 → 403
  • 다른 문서 id 토큰 → 403 (id 만 바꿔 긁는 경로)
  • claims 없음 → 403
  • 일치하는 토큰은 거부되지 않음 — 거부만 검증하면 비교를 뒤집어 놔도 전부 통과하므로 허용 경로도 확인한다

로컬에 Go 툴체인이 없어 CI 로 검증한다.

영향 범위

  • DB 마이그레이션: 없음
  • API contract 변경: 없음 (사용처 없는 경로의 접근 제어만 강화)
  • 환경변수: 없음

리뷰어 체크포인트

  • DOCUMENT 스코프 토큰을 Core 에서 발급할 계획이 있는지. 없다면 이 경로를 아예 지우는 것도 방법이다 — 지금은 패턴 일관성을 위해 남겨뒀다
  • 같은 맥락에서 /realtime/stream/me(유저 채널)는 토큰의 UserID 로 채널을 만들므로 id 조작 여지가 없다 — 확인했다

`/realtime/stream/documents/{id}` 만 토큰 리소스 범위를 확인하지 않고 있었다
(`// TODO: DOCUMENT 스코프 검증 (deferred)`). 세션 채널 3개는 모두
`ResourceType`/`ResourceID` 를 확인한다.

이 채널로 흐르는 건 `AnalysisCallbackPayload` 다 — 분석 **요약·기술스택·문서 S3 경로**.
즉 남의 이력서·자소서 내용이다. 유효한 스트림 토큰(로그인한 사용자면 자기 것으로 언제든
발급 가능)만 있으면 document id 를 바꿔가며 타인의 분석 결과를 구독할 수 있었다.
id 는 BIGSERIAL 이라 열거도 쉽다. 이벤트 replay 는 없으므로 "구독 중일 때 완료되는 분석"
으로 범위가 제한되지만, 연결을 열어두면 계속 수확할 수 있다.

세션 채널과 같은 방식으로 막는다. RealTime 은 DB 를 보지 않으므로, Core 가 소유권을 확인해
발급한 토큰의 리소스 범위를 강제하는 것이 유일한 소유권 검사다. `channelByPath` 를
`scopedChannel(sse, kind, resourceType)` 로 바꿨다.

현재 Core 는 DOCUMENT 스코프 토큰을 발급하지 않고, 프론트도 이 채널을 쓰지 않는다
(분석 상태는 `/realtime/stream/me` 유저 채널로 받는다 — AnalysisCallbackService 가 양쪽에
publish 한다). 따라서 이 변경으로 깨지는 사용처는 없고, 나중에 DOCUMENT 토큰을 도입하면
그대로 동작한다.

테스트 4종: 다른 리소스 타입(SESSION) 토큰 거부, 다른 문서 id 거부, claims 없음 거부,
그리고 **일치하는 토큰은 거부되지 않음**(거부만 검증하면 비교를 뒤집어도 통과한다).
§15 가 '리소스 소유권 검증 미구현' 이라고 적고 있었는데 세션 채널 3개는 이미 검증하고
있었다. 이번에 document 채널까지 붙였으므로 채널별 규칙을 표로 정리한다.
@i3months
i3months merged commit 6bb239f into dev Aug 19, 2026
4 checks passed
@i3months
i3months deleted the fix/document-stream-scope branch August 19, 2026 07:00
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