Skip to content

security(receiver): 기록함 접근 자격·마스터키 비노출 서버 계약을 확정하고 검증한다 #1949

Closed

Description

@OnlyReadOneBook

작업 유형

security — 보안 취약점·권한·자격·공급망 대응

주 담당 모듈

receiver — 수신자 관리

개요

#607의 record-boxes 연동 구현에서 PM 권한 결정과 안전한 서버 응답 준비를 분리한다. Afternote/Afternote-BE#93은 OPEN이며 같은 이메일에 연결된 다른 발신자의 accessCode 노출 문제와 목록 접근 자격 결정을 추적한다.

완료 조건:

  • 목록 접근 자격을 마스터키/이메일 소유 확인/회원 계정 중 무엇으로 증명할지 PM·BE의 확정 근거를 기록한다. 이 이슈가 임의로 방식을 선택하지 않는다.
  • 기록함 목록과 개별 콘텐츠 열람의 권한 범위를 분리하고, 다른 발신자의 마스터키를 목록 응답으로 획득하지 못하게 하는 최종 응답 계약을 확인한다.
  • BE#93 수정 배포와 테스트 데이터 기반 권한 거부·정상 목록 조회 검증을 확인한다. 타인 실데이터를 사용하지 않는다.
  • record-boxes의 안정적인 식별자, 상태/승인일, 인증 헤더와 개별 열람 수단을 #607에 인계한다. 6자리 이메일 인증번호와 UUID 마스터키의 용도를 혼동하지 않는다.

blocked_by: Afternote/Afternote-BE#93. 카카오 전환 #943/#944는 연관 계약으로 참고하되, 현행 권한 계약으로 독립 해결 가능하므로 BE#227을 이 이슈의 직접 선행으로 추가하지 않는다. #607은 본 이슈 완료 전 오래된 accessCode 포함 응답을 정본으로 구현하지 않는다.

참고

분리 원본: #607. 사용자 요청에 따른 수신자 블로킹 범위 분리(2026-09-08). 원본의 구현 책임은 유지하며 본 이슈는 선행 계약·결정 범위만 소유한다.

Activity

  1. added
    security보안 취약점·권한·자격·공급망 대응
    area:receiver수신자 관리 기능 영역
    on Sep 7, 2026
  2. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    Afternote-BE#93 에 코멘트로 아직 나간 적 없던 질문을 보냈다(2026-09-13).

    BE 저장소 실측을 함께 보냈다. deploy/sql/20260908_receiver_invitation.sql 은 accepted_user_id 를 NULL 로 추가만 하고 값을 채우는 UPDATE 가 없다. 같은 디렉터리의 다른 마이그레이션에는 UPDATE 가 있으므로 빠뜨린 것이 아니라 넣지 않은 것으로 보인다. 운영 DB 에서 수동 백필이 돌았는지는 저장소로 볼 수 없어 그 한 가지를 물었다.

    물은 내용은 묵은 본문을 옮긴 것이 아니라 Afternote-BE main 실코드로 대조해 다시 벼린 것이다. 코드로 이미 답이 나온 축은 질문에서 빼고 사실로 적었다.

  3. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    BE#93 을 닫아도 되는지 적대적으로 검증했고, 닫으면 안 된다는 결론이 나왔다. 앞선 코멘트에서 내가 잘못 읽은 곳도 함께 정정한다. BE 쪽 정정은 Afternote/Afternote-BE#93 (comment) 에 남겼다.

    판정 경로는 Afternote-BE origin/main(3593aa3) 실코드다.

    보안 결함 자체는 해소됐다

    accessCode 는 src·test 전체에서 0건이고 ReceivedRecordBoxResponse 에서 빠졌다. ReceiverAuthController 는 파일째 삭제됐고 receiver-auth 문자열도 0건이다. 격리도 코드상 실재한다. 목록은 findAllByAcceptedUserIdOrderByIdDesc(userId) 이고, 상세와 하위 경로는 전부 findByIdAndAcceptedUserId 를 먼저 타 없으면 RECEIVER_NOT_FOUND 로 끊긴다. 목록만 막히고 상세가 뚫리는 구멍은 없다.

    그런데 FE 가 알아야 할 것이 셋 있다

    1. BE#93 본문은 수신자가 자기 앞으로 온 발신자를 전부 보는 것이 기획 요구사항이라고 2026-07-29 PM 확인까지 붙여 적었고, 발신자를 하나씩 추가하는 방식은 이 요구에 어긋난다고 못 박았다. 그런데 PR build: 공용 debug keystore 도입 (신규 팀원 OAuth 동작 단순화) (#288) #289 의 구현이 그 방식이다. acceptedUserId 가 채워지는 자리는 Receiver.fromAcceptedInvitation 하나뿐이고 호출자는 초대 수락 하나다. 수신자는 이제 자기가 하나씩 수락한 발신자만 본다.

    2. 구 수신자는 영구히 빈 기록함이 된다. 마이그레이션이 accepted_user_id 를 NULL 로 추가만 하고 채우지 않으며 백필 코드도 없다. 그리고 이건 나중에 백필해서 풀 수 있는 문제가 아니다. 구 행의 email 은 발신자가 입력한 미검증 값이고 그것이 BE#93 이 지적한 공격의 전제라, 이메일 매칭 백필은 취약점을 되살린다. PM 이 요구를 철회하거나 대체 설계를 정해야 끝난다.

    3. receiver.auth_code DROP 은 파일에 대한 사실이지 운영 DB 에 대한 사실이 아니다. deploy.yml 은 deploy/sql 을 EC2 로 보내지 않고 prod 는 ddl-auto: update 라 컬럼을 DROP 하지 않는다. 구 마스터 키가 운영 DB 에 남아 있을 수 있다. 읽는 코드가 없어 API 노출은 아니다. 실행 여부를 BE 에 물어 뒀다.

    이 이슈의 상태

    FE 전환은 그대로 남는다. origin/develop 86ab223 는 아직 receiver-auth/record-boxes 와 X-Auth-Code, POST users/receivers 를 부른다. 다만 전환의 목표 동작이 1번 때문에 아직 확정이 아니다. 수신자에게 무엇이 보여야 하는지가 PM 결정에 걸려 있으므로, 그 답이 오기 전에는 화면 쪽 완료 조건을 확정하지 않는다.

    확인하지 못한 것을 적어 둔다. 실서버 HTTP 실측과 운영 DB 스키마는 자격이 필요해 하지 않았다.

  4. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    완료 조건 대조 (2026-09-13)

    판정 경로는 Afternote-BE origin/main 3593aa3(PR Afternote/Afternote-BE#292 머지 시점)과 Afternote-FE origin/develop 7d87aa3 실코드다. 앞선 두 코멘트에서 이미 적은 논거는 되풀이하지 않고 링크로 잇는다(BE#93 재검증, BE 에 보낸 질문). 여기서는 이 이슈의 완료 조건 네 개를 항목별로 채운다.

    1. 목록 접근 자격의 증명 수단: 수단은 확정, 범위는 미확정

    수단은 회원 계정이다. 마스터키도 이메일 소유 확인도 아니다.

    • src/main/java/com/afternote/domain/receiver/controller/ReceivedRecordController.java:21 이 /api/v1/received-records 를 잡고, 목록 핸들러(:29-33)의 인자는 @UserId Long userId 하나다. 헤더 파라미터가 없다.
    • 자격이 붙는 자리는 src/main/java/com/afternote/domain/receiver/model/Receiver.java:33-34 의 accepted_user_id 이고, 값이 채워지는 경로는 Receiver.java:77(초대 수락) 하나다.
    • 근거 PR 은 [feat] 카카오톡 초대 기반 수신자 등록 및 JWT 인증 전환 Afternote-BE#289(2026-09-08 머지, 87fdbf1).

    이 이슈가 방식을 고른 것이 아니라 BE 구현이 이미 그렇게 들어와 있는 상태를 기록한다. 다만 PM 확정 근거로는 아직 모자라다. BE#93 본문이 적은 기획 요구(수신자가 자기 앞으로 온 발신자를 전부 본다, 2026-07-29 PM 확인)와 지금 구현(초대 수락한 발신자만 본다)이 어긋나는 문제를 BE#93 에 물어 뒀고 답이 오지 않았다. 수단은 확정, 수신자에게 무엇이 보여야 하는지는 미확정이다.

    2. 목록과 개별 열람의 권한 분리, 마스터키 비노출 응답 계약: 채움

    응답 계약에서 마스터키가 사라졌다.

    • src/main/java/com/afternote/domain/receiver/dto/ReceivedRecordBoxResponse.java:13-71 의 필드는 receiverId·senderName·receiverName·relation·recordStatus·viewStatus·verificationStatus·requestedAt·approvedAt 아홉 개이고 accessCode 가 없다.
    • 저장소 전역 코드 검색에서 accessCode 0건, ReceiverAuthController 0건이다. receiver-auth 문자열은 scripts/contract-test.sh:28 의 제외 정규식 한 건만 남았고 이건 매핑이 아니라 잔재다.
    • Receiver.java 에 auth_code 필드가 없고, deploy/sql/20260908_receiver_invitation.sql:106-123 이 컬럼을 DROP 한다(파일 기준 사실이다. 운영 DB 반영 여부는 3번).

    권한 분리도 코드로 실재한다.

    • 목록: src/main/java/com/afternote/domain/receiver/service/ReceivedRecordService.java:42 가 findAllByAcceptedUserIdOrderByIdDesc(userId).
    • 개별과 하위 경로: 같은 파일 :54·:60·:66·:70·:75·:82·:90·:97·:101·:109·:117·:127 이 전부 findAcceptedReceiver 를 먼저 타고, 그 구현(:133-135)은 findByIdAndAcceptedUserId 가 비면 RECEIVER_NOT_FOUND 로 끊는다.
    • ReceiverRepository.java:15-18 에 이메일로 찾는 메서드가 없다. 조회 키는 userId·acceptedUserId 뿐이다.

    목록만 막히고 상세가 뚫리는 구멍은 코드상 없다.

    3. 수정 배포와 권한 거부·정상 조회 검증: 못 채움

    • [fix] 받은 기록함 목록 응답에 다른 발신자의 마스터 키가 포함됨 Afternote-BE#93 은 여전히 OPEN 이다(2026-09-13 확인).
    • 배포 반영 여부를 저장소로 판정할 수 없다. deploy.yml 이 deploy/sql 을 서버로 보내지 않고 prod 는 ddl-auto: update 라 컬럼을 DROP 하지 않는다는 것까지가 저장소로 보이는 전부다. 실행 여부는 BE 에 물어 둔 상태다.
    • 테스트 데이터 기반 권한 거부와 정상 목록 조회는 실측하지 않았다. 지금 적은 격리는 코드 경로를 읽은 결과이지 HTTP 응답을 받아 본 결과가 아니다. 실서버 호출과 운영 DB 스키마 조회는 자격이 필요해 하지 않았다.

    이 항목은 BE#93 이 닫히고 dev 서버에 반영된 뒤 별도 실측으로 채운다.

    4. #607 인계: 계약은 확정, 인계 자체는 남음

    인계할 내용은 다음으로 확정한다.

    • 안정적 식별자: receiverId(Long). 구 계약에서 칸을 골라내던 키는 마스터키였고(feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/senderdetail/SenderDetailViewModel.kt:183 의 it.masterKey == masterKey), 새 계약에는 그 값이 없으므로 선택 키가 receiverId 로 바뀐다.
    • 상태와 승인일: recordStatus·viewStatus·verificationStatus·requestedAt·approvedAt. approvedAt 은 ReceivedRecordBoxResponse.java:99-102 대로 APPROVED 일 때만 채워진다.
    • 인증 수단: 액세스 토큰(@UserId). X-Auth-Code 헤더는 서버 계약에서 사라졌다.
    • 개별 열람 수단: GET /api/v1/received-records/{receiverId} 와 그 하위(/time-letters, /after-notes, /diary, /deep-thought, /daily-question, /message, /presigned-url, /delivery-verification, /delivery-verification/status). 전부 ReceivedRecordController.java:35-163.
    • 초대 수락 경로: POST /api/v1/receiver-invitations/{token}/accept(ReceiverInvitationController.java:47).
    • 6자리 이메일 인증번호와 UUID 마스터키의 용도 구분: 새 계약에는 둘 다 없다. receiver-auth/email/auth-code·receiver-auth/email/verify 를 포함해 receiver-auth 접두 매핑이 BE 소스에 0건이다.

    FE 가 아직 구 계약인 지점은 다음과 같다(origin/develop 7d87aa3).

    • feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/service/ReceiverAuthApiService.kt:65 가 @GET("receiver-auth/record-boxes") 다.
    • feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/dto/ReceiverAuthDto.kt:132 의 @SerialName("accessCode") val accessCode: String 은 non-null 이고 :228 에서 masterKey 로 옮겨 담는다. 서버가 이 필드를 안 내려주는 순간 목록 파싱이 예외로 떨어진다. 회귀 가드 feature/receiver/data/src/test/kotlin/com/afternote/feature/receiver/data/dto/ReceivedRecordBoxContractTest.kt 도 같은 필드를 고정하고 있어 함께 갈아야 한다.
    • feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/network/ReceiverAuthInterceptor.kt:32 가 /receiver-auth/ 경로에 X-Auth-Code 를 자동 부착한다.
    • core/network/src/main/kotlin/com/afternote/core/network/service/UserApiService.kt:39 가 @POST("users/receivers") 다. BE UserController 에 같은 매핑이 없다(GET·PATCH·DELETE 만 남았다).
    • 전환 범위는 record-boxes 하나가 아니다. main 소스의 receiver-auth 엔드포인트 선언이 14건이고, mindrecord·timeletter·afternote 모듈에 걸쳐 있다.

    인계가 아직 끝나지 않은 이유는 #607 에 이 계약이 적혀 있지 않아서다. #607 의 마지막 실측 코멘트는 2026-08-25 이고 구 계약(X-Auth-Code, receiver-auth/delivery-verification/status) 기준이다. #607 본문과 코멘트를 새 계약으로 갱신하는 것이 이 항목의 남은 일이다.

    남은 것

    • 3번 전체. BE#93 이 닫히고 dev 서버에 반영된 뒤 권한 거부와 정상 조회를 실측한다.
    • 1번의 범위 축. 수신자에게 어떤 발신자가 보여야 하는지가 PM 결정에 걸려 있다. 구 수신자 백필이 이메일 매칭으로는 취약점을 되살리므로 백필로 우회할 수 없다는 점도 함께 물어 둔 상태다.
    • 4번의 인계 기록. 위 계약을 받은 기록함이 서버 목록 API 를 쓰지 않고 로컬 stub 으로 동작 — record-boxes 미연동 #607 에 옮긴다.
    • 운영 DB 의 receiver.auth_code 실행 여부. 읽는 코드가 없어 API 노출은 아니지만 컬럼이 남아 있을 수 있다.

    3번이 남아 있으므로 이 이슈는 닫지 않는다. blocked_by 는 Afternote/Afternote-BE#93 그대로다.

  5. OnlyReadOneBook commented on Sep 19, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    2026-09-19 정리로 닫는다. 이 이슈의 완료 조건은 PM의 결정에 달려 있어 결정 전에는 FE 가 무엇을 만들지 쓸 수 없다. 그런 이슈는 FE 저장소에 열어 두지 않고 결정이 나면 그때 새 이슈로 다시 연다.

    기다리는 결정: 기록함 접근 자격 방식(BE#93 포함).

    PM 질문은 창구가 정해지는 대로 보낸다(일혁이 목록 보관).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:receiver수신자 관리 기능 영역security보안 취약점·권한·자격·공급망 대응

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions