You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(receiver): 기록함 접근 자격·마스터키 비노출 서버 계약을 확정하고 검증한다 #1949
#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). 원본의 구현 책임은 유지하며 본 이슈는 선행 계약·결정 범위만 소유한다.
BE 저장소 실측을 함께 보냈다. deploy/sql/20260908_receiver_invitation.sql 은 accepted_user_id 를 NULL 로 추가만 하고 값을 채우는 UPDATE 가 없다. 같은 디렉터리의 다른 마이그레이션에는 UPDATE 가 있으므로 빠뜨린 것이 아니라 넣지 않은 것으로 보인다. 운영 DB 에서 수동 백필이 돌았는지는 저장소로 볼 수 없어 그 한 가지를 물었다.
물은 내용은 묵은 본문을 옮긴 것이 아니라 Afternote-BE main 실코드로 대조해 다시 벼린 것이다. 코드로 이미 답이 나온 축은 질문에서 빼고 사실로 적었다.
accessCode 는 src·test 전체에서 0건이고 ReceivedRecordBoxResponse 에서 빠졌다. ReceiverAuthController 는 파일째 삭제됐고 receiver-auth 문자열도 0건이다. 격리도 코드상 실재한다. 목록은 findAllByAcceptedUserIdOrderByIdDesc(userId) 이고, 상세와 하위 경로는 전부 findByIdAndAcceptedUserId 를 먼저 타 없으면 RECEIVER_NOT_FOUND 로 끊긴다. 목록만 막히고 상세가 뚫리는 구멍은 없다.
그런데 FE 가 알아야 할 것이 셋 있다
BE#93 본문은 수신자가 자기 앞으로 온 발신자를 전부 보는 것이 기획 요구사항이라고 2026-07-29 PM 확인까지 붙여 적었고, 발신자를 하나씩 추가하는 방식은 이 요구에 어긋난다고 못 박았다. 그런데 PR build: 공용 debug keystore 도입 (신규 팀원 OAuth 동작 단순화) (#288) #289 의 구현이 그 방식이다. acceptedUserId 가 채워지는 자리는 Receiver.fromAcceptedInvitation 하나뿐이고 호출자는 초대 수락 하나다. 수신자는 이제 자기가 하나씩 수락한 발신자만 본다.
구 수신자는 영구히 빈 기록함이 된다. 마이그레이션이 accepted_user_id 를 NULL 로 추가만 하고 채우지 않으며 백필 코드도 없다. 그리고 이건 나중에 백필해서 풀 수 있는 문제가 아니다. 구 행의 email 은 발신자가 입력한 미검증 값이고 그것이 BE#93 이 지적한 공격의 전제라, 이메일 매칭 백필은 취약점을 되살린다. PM 이 요구를 철회하거나 대체 설계를 정해야 끝난다.
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 스키마는 자격이 필요해 하지 않았다.
판정 경로는 Afternote-BE origin/main 3593aa3(PR Afternote/Afternote-BE#292 머지 시점)과 Afternote-FE origin/develop7d87aa3 실코드다. 앞선 두 코멘트에서 이미 적은 논거는 되풀이하지 않고 링크로 잇는다(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(초대 수락) 하나다.
이 이슈가 방식을 고른 것이 아니라 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 뿐이다.
안정적 식별자: 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건이다.
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 결정에 걸려 있다. 구 수신자 백필이 이메일 매칭으로는 취약점을 되살리므로 백필로 우회할 수 없다는 점도 함께 물어 둔 상태다.
Activity
OnlyReadOneBook commentedon Sep 13, 2026
Afternote-BE#93 에 코멘트로 아직 나간 적 없던 질문을 보냈다(2026-09-13).
BE 저장소 실측을 함께 보냈다. deploy/sql/20260908_receiver_invitation.sql 은 accepted_user_id 를 NULL 로 추가만 하고 값을 채우는 UPDATE 가 없다. 같은 디렉터리의 다른 마이그레이션에는 UPDATE 가 있으므로 빠뜨린 것이 아니라 넣지 않은 것으로 보인다. 운영 DB 에서 수동 백필이 돌았는지는 저장소로 볼 수 없어 그 한 가지를 물었다.
물은 내용은 묵은 본문을 옮긴 것이 아니라 Afternote-BE main 실코드로 대조해 다시 벼린 것이다. 코드로 이미 답이 나온 축은 질문에서 빼고 사실로 적었다.
OnlyReadOneBook commentedon Sep 13, 2026
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 가 알아야 할 것이 셋 있다
BE#93 본문은 수신자가 자기 앞으로 온 발신자를 전부 보는 것이 기획 요구사항이라고 2026-07-29 PM 확인까지 붙여 적었고, 발신자를 하나씩 추가하는 방식은 이 요구에 어긋난다고 못 박았다. 그런데 PR build: 공용 debug keystore 도입 (신규 팀원 OAuth 동작 단순화) (#288) #289 의 구현이 그 방식이다.
acceptedUserId가 채워지는 자리는Receiver.fromAcceptedInvitation하나뿐이고 호출자는 초대 수락 하나다. 수신자는 이제 자기가 하나씩 수락한 발신자만 본다.구 수신자는 영구히 빈 기록함이 된다. 마이그레이션이
accepted_user_id를 NULL 로 추가만 하고 채우지 않으며 백필 코드도 없다. 그리고 이건 나중에 백필해서 풀 수 있는 문제가 아니다. 구 행의 email 은 발신자가 입력한 미검증 값이고 그것이 BE#93 이 지적한 공격의 전제라, 이메일 매칭 백필은 취약점을 되살린다. PM 이 요구를 철회하거나 대체 설계를 정해야 끝난다.receiver.auth_codeDROP 은 파일에 대한 사실이지 운영 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 스키마는 자격이 필요해 하지 않았다.
OnlyReadOneBook commentedon Sep 13, 2026
완료 조건 대조 (2026-09-13)
판정 경로는 Afternote-BE
origin/main3593aa3(PR Afternote/Afternote-BE#292 머지 시점)과 Afternote-FEorigin/develop7d87aa3 실코드다. 앞선 두 코멘트에서 이미 적은 논거는 되풀이하지 않고 링크로 잇는다(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(초대 수락) 하나다.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가 없다.accessCode0건,ReceiverAuthController0건이다.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. 수정 배포와 권한 거부·정상 조회 검증: 못 채움
deploy.yml이deploy/sql을 서버로 보내지 않고 prod 는ddl-auto: update라 컬럼을 DROP 하지 않는다는 것까지가 저장소로 보이는 전부다. 실행 여부는 BE 에 물어 둔 상태다.이 항목은 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).receiver-auth/email/auth-code·receiver-auth/email/verify를 포함해receiver-auth접두 매핑이 BE 소스에 0건이다.FE 가 아직 구 계약인 지점은 다음과 같다(
origin/develop7d87aa3).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")다. BEUserController에 같은 매핑이 없다(GET·PATCH·DELETE만 남았다).receiver-auth엔드포인트 선언이 14건이고, mindrecord·timeletter·afternote 모듈에 걸쳐 있다.인계가 아직 끝나지 않은 이유는 #607 에 이 계약이 적혀 있지 않아서다. #607 의 마지막 실측 코멘트는 2026-08-25 이고 구 계약(
X-Auth-Code,receiver-auth/delivery-verification/status) 기준이다. #607 본문과 코멘트를 새 계약으로 갱신하는 것이 이 항목의 남은 일이다.남은 것
receiver.auth_code실행 여부. 읽는 코드가 없어 API 노출은 아니지만 컬럼이 남아 있을 수 있다.3번이 남아 있으므로 이 이슈는 닫지 않는다. blocked_by 는 Afternote/Afternote-BE#93 그대로다.
OnlyReadOneBook commentedon Sep 19, 2026
2026-09-19 정리로 닫는다. 이 이슈의 완료 조건은 PM의 결정에 달려 있어 결정 전에는 FE 가 무엇을 만들지 쓸 수 없다. 그런 이슈는 FE 저장소에 열어 두지 않고 결정이 나면 그때 새 이슈로 다시 연다.
기다리는 결정: 기록함 접근 자격 방식(BE#93 포함).
PM 질문은 창구가 정해지는 대로 보낸다(일혁이 목록 보관).