웹 브라우저에 통수를 맞았다고 잘못된 의심을 품은, 무지했던 그 날을 반성하며...
제가 겪었던 이슈를 설명하기 위해서는 저에 대한 간단한 소개가 필요할 것 같네요.
저는 현재 한 회사에서 타 개발 팀을 주 대상으로 하여 기술적으로 지원하는 서비스 서포트팀에 소속되어 근무 중에 있습니다.
지원해드리는 팀 중 한 팀인 A팀 이 현재 서버 프로그래머를 구하기 어려운 상황이라, 서버 프로그래머 경력이 있는 제가 해당 팀의 서버 개발을 도와드리고 있습니다.
그 팀을 도와드리던 중에 이 이슈를 마주치게 되었습니다...
여느 때와 다르지 않게 다른 팀들을 지원하기 위한 업무를 진행하던 도중, A팀에서 기능 추가를 요청하셨고, 그 기능은 다음과 같았습니다.
검증은 다음과 같은 단계로 진행하게 됩니다.
페이지 레이아웃 및 코드 작업은 완료되었으며, 대략 다음과 같이 Form 을 사용하여 로그인 정보를 POST 하도록 구성되어 있었습니다.
<form action="/signin">
<input id="address" name="address" />
<input id="message" name="message" />
<input id="signature" name="signature" />
</form>
서명 API 호출에 사용된 메시지는 대략 다음과 같은 코드를 통해 생성됩니다.
function message(now: Date): string {
return `Confirm to login\n${now.getTime()}`;
}
백엔드에서는 위에서 보여드린 Form 에서 전달받은 데이터를 대략 다음과 같은 코드를 통해 검증하게 됩니다 (백엔드는 C#과 ASP.NET 를 사용하여 작성되어 있습니다).
string address = Request.Form["address"].FirstOrDefault("");
string message = Request.Form["message"].FirstOrDefault("");
string signature = Request.Form["signature"].FirstOrDefault("");
// 유효성 검증
var signer = new EthereumMessageSigner();
string recoveredAddress = signer.EncodeUTF8AndEcRecover(
message,
signature);
if (!string.Equals(address, recoveredAddress))
{
return BadRequest("invalid");
}
// 검증 통과 시 실행될 코드의 위치
return Ok();
이제 검증 코드 작성은 끝났으니 테스트를 진행해서 이상이 없으면 코드리뷰를 넘기면 됩니다.
“라이브러리 공식 문서에 명시된 것을 확인하고 작성한 코드니 테스트는 손쉽게 통과되겠구만~!” 이라고 생각하고 테스트를 진행하였습니다.
그리고, 안일한 생각이었다는 걸 뒤늦게 깨달았습니다...
가벼운 마음으로 테스트를 진행했는데, 계속 검증 실패가 떨어졌습니다.
사용한 라이브러리 공식 문서를 뒤져도 보고, 스택오버플로우 등에서 검색을 해봐도 사용한 코드에는 별 문제가 없는데 검증은 계속 실패하였고, 그렇게 디버깅과 코드만 뒤져보며 아무 소득없이 두 시간을 날리고 있었습니다.
그 때, 디버깅을 진행하던 도중 메시지 값을 확인했는데, 값이 조금 이상함을 눈치챘습니다.
"Confirm to login\r\n1675445887000"
눈치채셨나요?
분명 프론트엔드 코드에서 생성한 메시지에는 LF (\n) 만 사용하였는데, 서버에서 전달받은 메시지에는 CRLF (\r\n) 가 들어있었습니다...!
갑자기 마주하게 된 이 상황에 당황하면서 다시 작업된 프론트엔드 코드에 문제가 없는지 확인해봤지만, 따로 LF 를 CRLF 로 변환하는 코드는 존재하지 않았습니다.
혼란에 빠진 상태로 검색에 들어갔지만 별다른 소득이 없이 또 한 시간이 흘렀고, 지치기 시작한 저는 다음과 같이 의심하게 됩니다.
내가 사용 중인 OS 가 Windows 라 Chrome Browser 에서 개행을 \r\n 로 멋대로 변환한거 아냐?!
의심을 가지고 여러 키워드로 검색해봤지만, 별다른 소득은 없이 또 약 한 시간이 흐르게 되었고, 지친 저는 마지막으로 ‘chrome form crlf’ 라는 키워드로 검색을 해보고 소득이 없다면 원인 파악은 다음으로 미루려고 했습니다.
제가 지쳤다는 것을 누군가 알고 도움을 준 것일까요? 다음과 같은 제목을 가진 스택오버플로우 질문 글이 검색 결과로 나왔습니다.
Firefox and Chrome replacing LF with CR+LF during POST
제목을 본 저는 이런 생각이 들었습니다.
옳거니! 역시 브라우저가 맘대로 바꾸는거네!
아무래도 지쳤던 탓일까요? 나무랄 대상이 생겨서 기뻣던 것 같습니다 ㅋㅋㅋ;;
하지만 이 생각이 잘못된 생각이고, 자신의 무지를 알게 되기까지는 얼마 걸리지 않았습니다. 해당 질문에 대한 대답이 다음과 같았거든요.
Turns out that this has to do with the x-www-form-urlencoded encoding type. According to the spec:
Non-alphanumeric characters are replaced by '%HH', a percent sign and two hexadecimal digits representing the ASCII code of the character. Line breaks are represented as "CR LF" pairs (i.e., '%0D%0A').
그렇습니다. 이 동작은 전혀 문제없는, 표준에 의해 정의된 동작이었던 것입니다.
또 한 번 당황했지만, 우선 표준에 의한 동작이라는 것은 알았으니 이슈를 먼저 수정한 후 테스트를 진행했고, 검증 통과를 눈으로 확인한 후 기쁘게 코드 리뷰를 등록하고 퇴근하였습니다.
그렇게 퇴근하던 중, 문득 이런 생각이 들더군요.
근데 프론트엔드에서 해당 엘리먼트의 DOM 객체에 접근해서 값을 얻으면 \n 이 그대로 나오던데?
그 생각을 가지고 집에 도착한 후, 품고 있던 궁금증을 해소하기 위해 바로 컴퓨터 앞에 앉아 다음 목록을 정리하고, 검색하기 시작했습니다.
검색을 하면 할수록, 저의 무지와 근거없는 브라우저 의심에 반성하는 시간이 되었습니다....
처음 쓰는 블로그 포스팅이라 글이 난잡하고 애매한 타이밍에 끊게 되었네요 ㅎㅎ;;; 다음 글에서는 위에서 말씀드린 내용들을 검색해서 알게 된 내용을 정리해서 공유해보도록 하겠습니다.
여담으로, 이런 비슷한 일을 종종 겪을 때마다 항상 다음과 같이 반성하게 됩니다.
근거없는 추측을 확신하지 말고, 표준 명세나 공식 문서를 확인하고 이슈를 정확히 파악하자.