Skip to content

임베딩 검색을 요청자 소유 문서로 제한 - #217

Merged
i3months merged 1 commit into
devfrom
feat/embedding-search-user-scope
Aug 23, 2026
Merged

임베딩 검색을 요청자 소유 문서로 제한#217
i3months merged 1 commit into
devfrom
feat/embedding-search-user-scope

Conversation

@i3months

Copy link
Copy Markdown
Member

문제

#216 을 작업하며 함께 보고했던 건이다. POST /api/internal/embeddings/search 에는 userId 파라미터가 아예 없었다.

  • documentIds 가 비면 스펙상 "전체 검색" — 다른 사용자의 청크까지 대상
  • id 를 줘도 소유권을 확인하지 않는다 — 남의 문서 id 를 넣으면 그대로 조회
  • document_embeddings 에는 청크 원문이 저장되므로, 나오면 곧 남의 이력서 문장이다

실제 유출은 없었다. AI 호출부 3곳(questions/followup/feedback)이 모두 빈 목록을 사전에 걸러 (none) 을 반환한다. 확인했다.

문제는 방어가 전적으로 호출자에게 있었다는 점이다. 호출부가 하나 늘거나 가드를 빠뜨리면 남의 이력서 청크가 프롬프트로 들어간다. 내부 API 라 X-Internal-API-Key 뒤에 있지만, 그건 "AI 서버가 맞나"만 확인할 뿐 "누구 데이터를 볼 자격이 있나"는 묻지 않는다.

수정

Core 가 스코프를 확정한다 — 호출자가 무엇을 보내든 요청자 소유 문서를 벗어날 수 없다.

입력 동작
userId 필수 (@NotNull)
documentIds 있음 소유 문서와의 교집합만 — 요청한 id 를 그대로 믿지 않는다
documentIds 비음 그 사용자의 활성 문서 전체 ("비면 전체 사용자" 규약 폐기)
교집합이 빔 검색하지 않고 빈 결과 — 빈 목록을 그대로 넘기면 다시 전체 검색이 된다

AI 는 envelope.context.user_id 를 싣는다. Core 가 generate.questions/followup/feedback 발행 시 MessageContext 에 이미 userId 를 채우고 있고 AI 모델에도 필드가 있어서 — 메시지 계약 변경이 없다. 세 consumer 의 _process(envelope) 에서 검색까지 값을 흘리기만 하면 됐다.

user_id 를 못 얻는 경우(구버전 발행 등)는 검색을 건너뛰고 (none) 으로 폴백한다. RAG 는 보강용이라 없어도 생성은 진행된다.

테스트

Core (#197 인프라, 실제 pgvector):

  • serviceScopesSearchToRequestingUsersDocuments — 남의 문서 id 를 명시해도 결과에 없다. documentIds 를 비워도 전체 검색이 되지 않는다
  • serviceReturnsEmptyWhenUserOwnsNothing — 소유 문서가 없으면 빈 결과 (빈 목록 위임 금지)

AI: 기존 test_search_embeddings_uses_latest_core_contract 가 새 필수 인자를 바로 잡아냈다(계약 테스트가 제 역할을 했다). 요청 body 기대값에 userId 를 반영. 393 tests 통과.

배포 순서

deploy-app.yml 이 백엔드·AI 를 같은 워크플로에서 배포한다. 그 사이 짧은 창에서 구버전 AI 가 userId 없이 호출하면 400 → AI 는 예외를 잡아 (none) 으로 폴백한다. 면접·피드백 생성은 계속되고 RAG 보강만 잠시 빠진다 — 실패 경로가 이미 non-fatal 로 설계돼 있어 별도 조치가 필요 없다.

문서

docs/messaging.md §10.1 신설 — 스코프 규약, 이전 규약 폐기, user_id 부재 시 폴백을 명시.

POST /api/internal/embeddings/search 에는 userId 파라미터가 아예 없었다.
documentIds 가 비면 스펙상 "전체 검색" 이라 다른 사용자의 청크까지 대상이고,
id 를 줘도 소유권을 확인하지 않아 남의 문서 id 를 넣으면 그대로 조회됐다.

실제 유출은 없었다 — AI 호출부 3곳이 모두 빈 목록을 사전에 걸러 (none) 을
반환한다. 하지만 방어가 전적으로 호출자에게 있었다. 호출부가 하나 늘거나
가드를 빠뜨리면 남의 이력서 청크가 프롬프트로 들어간다.

Core 가 스코프를 확정한다:
- userId 필수(@NotNull)
- documentIds 를 주면 소유 문서와의 교집합만 — 요청한 id 를 그대로 믿지 않는다
- 비면 그 사용자의 활성 문서 전체 ("비면 전체 사용자" 규약 폐기)
- 교집합이 비면 검색하지 않고 빈 결과 (빈 목록을 넘기면 다시 전체 검색이 된다)

AI 는 envelope.context.user_id 를 싣는다 — Core 가 generate.questions/
followup/feedback 발행 시 이미 채우고 있어서 메시지 계약 변경이 없다.
user_id 를 못 얻으면 검색을 건너뛰고 (none) 으로 폴백한다.
@i3months
i3months merged commit 8ac5d7c into dev Aug 23, 2026
5 checks passed
@i3months
i3months deleted the feat/embedding-search-user-scope branch August 23, 2026 16:35
i3months added a commit to i3months/stackup that referenced this pull request Aug 28, 2026
지금까지 자료 삭제는 soft delete 만 했다. S3 의 업로드 원본 PDF·분석 마크다운과
document_embeddings 의 청크가 그대로 남았다. 이력서 PDF 에는 이름·연락처·주소가
들어 있고(docs/security.md §5.2) 임베딩 청크에는 그 원문 조각이 들어 있다.
Team-StackUp#216 으로 검색에서 빠지고 Team-StackUp#217 로 스코프가 닫혀 읽히지는 않게 됐지만,
보관 자체가 남아 있었다.

행은 계속 soft delete 로 남긴다 — session_contexts 가 analyzed_documents 를
FK 로 참조해서 행을 지우면 제약 위반이다. 남길 이유가 있는 건 참조 무결성뿐이고
내용물은 아니다.

- 임베딩 청크: 삭제와 같은 트랜잭션에서 DELETE
- 스토리지 객체: ObjectPurgeEvent → AFTER_COMMIT 리스너
  커밋 전에 지우면 롤백된 삭제로 객체만 사라져 되돌릴 수 없다
- 스토리지 삭제 실패는 사용자 요청을 실패시키지 않는다. 이미 커밋돼 행이 도달
  불가이고 예외를 던져도 삭제를 되돌릴 수 없다. 키를 ERROR 로 남겨 수동 회수만
  가능하게 한다 — 조용히 삼키면 파기됐다고 착각하게 된다

이력서·레포·자소서 세 경로 모두 적용(AnalyzedDocumentCascadeListener 가 이미
세 이벤트를 다룬다). 웹 이력서는 업로드 원본이 없어 filePath 가 null 이고,
분석 전에 지운 자료는 documentPath 가 없다 — 빈 키는 이벤트에서 걸러진다.
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