얼쑤의 분석 서버는 요청에 담긴 서명 URL로 녹음을 내려받습니다. URL에 읽기 권한이 담겨 있어 유효 시간 안에는 주소만으로 파일을 요청할 수 있습니다. 분석 API를 구현할 때 이 주소가 어디에 남는지 점검했습니다. 제가 작성한 코드에서 URL을 로그에 쓰지 않아도 두 곳에 남을 수 있었습니다. 요청을 검증하는 프레임워크의 응답과 녹음을 내려받는 HTTP 라이브러리의 로그입니다.

BPM을 누락해야 드러난 응답 노출

당시 FastAPI 기본 검증 응답은 HTTP 422와 함께 오류가 난 입력값을 input에 넣었습니다. bpm에 0을 보내면 오류의 입력값도 0이었습니다. 이 경우만 검사하면 기존 처리기를 써도 서명 URL이 응답에 없었습니다. bpm을 아예 빼자 검증기가 그 필드가 있어야 할 요청 객체를 오류에 담았습니다. 같은 객체의 audioUrl도 응답에 실릴 수 있었습니다.

RequestValidationError 처리기를 등록해 오류마다 필드 위치와 오류 종류만 보내도록 바꿨습니다. 호출자는 어떤 값을 빠뜨렸는지 알 수 있고, 응답에 원본 요청이 복사되지 않습니다. 회귀 테스트도 잘못된 숫자를 주는 경우에서 필드를 누락하는 경우로 바꿨습니다.

실제 다운로드에서 드러난 로그 노출

HTTPX는 INFO에서 요청 메서드와 URL 전체를 기록했습니다. 주소에 붙은 서명도 함께 남았습니다. 다운로드 함수의 결과만 가짜로 돌려주는 테스트는 HTTPX를 호출하지 않으므로 이 문제를 잡을 수 없었습니다. 앱의 루트 로그를 DEBUG로 열어도 이 요청 줄이 나오지 않도록 HTTPX와 httpcore 로거의 수준을 WARNING으로 지정했습니다.

예외도 따로 확인했습니다. 예상한 다운로드 실패는 URL을 빼고 고정된 502 오류로 돌려줍니다. 분석 중 예상 밖의 예외는 요청 처리 경계에서 받아 정해진 500 본문을 반환합니다. 그때 로그에는 예외의 종류와 세션 번호, 시도 번호, 처리 시간을 남겼습니다. 원인을 좁힐 단서는 유지하면서 예외 메시지에 섞인 주소가 함께 기록되는 길을 닫았습니다.

응답과 로그를 따로 대조

로컬에서 실제 분석 API를 띄우고 저장소 자리에 별도 HTTP 서버를 놓았습니다. 합성 M4A, 없는 파일, 필수 필드를 뺀 요청을 각각 보내 응답을 확인하고, 캡처한 DEBUG 로그에서 테스트용 서명 토큰을 찾았습니다.

기존 실행 검증에서 보낸 요청관측한 응답
저장소 대역의 합성 M4AHTTP 200, 분석 완료
저장소 대역에 없는 파일HTTP 502
bpm을 뺀 요청HTTP 422, 원본 입력 필드 없음

이 실행에서 DEBUG 로그에 테스트용 서명 토큰이 나온 횟수는 0회였습니다. 자동 검사에서는 URL이 들어 있는 예외를 일부러 던져 상태 코드와 함께 500 본문이 정해진 값인지 확인했습니다. 422 처리기를 제거하거나 HTTPX의 INFO 로그를 다시 허용했을 때는 해당 회귀 테스트가 실패했습니다. 검사한 범위는 이 로컬 실행과 자동 테스트가 거친 응답·로그 경로입니다.

검증기와 다운로드 함수는 다른 곳을 대역으로 바꿨습니다

필수 필드가 없는 요청을 검사할 때는 FastAPI 앱을 그대로 두고 다운로드 의존성만 대역으로 바꿨습니다. bpm을 뺀 요청에서 422와 함께 다운로드 대역의 호출 0회를 확인했습니다. 검증에서 이미 거절한 요청이 저장소까지 가지 않았다는 근거입니다. 같은 대역이 DownloadError를 던지면 API가 502를 반환하는지도 확인했습니다. FastAPI 테스트 의존성 재정의

다운로드 함수 자체의 오류를 보려면 그 함수는 실행해야 합니다. 이 검사에서는 HTTPX의 전송 계층만 MockTransport로 바꿨습니다. 정상 응답에서는 파일의 바이트를 대조했습니다. 403 응답, 제한보다 1바이트 큰 본문, 연결 예외는 모두 다운로드 오류가 됐습니다. 403과 연결 예외에 넣은 가짜 서명이, 연결된 예외까지 출력한 문자열에도 없다는 것을 확인했습니다. 두 대역 검사가 다루는 범위는 요청 검증과 다운로드 함수의 오류 처리입니다. 운영 버킷의 권한이나 실제 네트워크는 이 검사에 포함되지 않습니다. HTTPX의 MockTransport

오류 문구에서 뺀 경로가 예외 객체에 남는 경우

녹음을 디코딩하는 FFmpeg도 표준 오류에 입력 파일의 위치를 포함할 수 있습니다. 디코딩 실패를 설명할 때는 경로를 <input>으로 바꿨습니다. 도구 실행이 시간 초과된 경우에는 Python의 TimeoutExpired 예외 자체가 실행 인자를 갖고 있었습니다. raise ... from None으로 기본 출력에서 예외 연결을 숨겨도, 원래 예외가 새 객체의 __context__에 남을 수 있습니다. Python의 예외 연결 문서

그래서 시간 초과를 잡는 블록을 벗어난 다음, 제한 시간만 담은 새 오류를 올렸습니다. 손상된 합성 파일에는 찾기 쉬운 경로 표식을 넣어 오류 문구에 나오지 않는지 확인했습니다. 별도의 시간 초과 검사에서는 문구에 더해 __context__와 __cause__가 비어 있는지도 확인했습니다. 각각 디코더의 표준 오류와 Python 예외 객체가 노출될 수 있는 경로를 확인한 결과입니다.

백엔드 요청 객체의 문자열 검사

Kotlin 백엔드는 분석 서버에 요청을 보낼 때 서명 URL을 요청 객체에 담습니다. 객체가 로그에서 문자열로 바뀔 때는 세션 번호, 시도 번호, 음표 개수만 남도록 AnalysisRequest.toString()을 따로 정의했습니다. 단위 검사에서는 가짜 서명값을 넣은 요청 객체를 문자열로 바꿔 그 값이 나오지 않는지 확인했습니다.