에러 없이 다른 계정으로 로그인되는 버그
개발 초기에 테스트 서버에서 개발을 하던 중 신기한 현상을 발견했다.
클라이언트에서 테스트 서버에 접속할 경우, 의도한 유저가 아닌 다른 유저로 접속되는 현상이었다.
아예 접속이 안 되는 것도 아니고, 접속은 되는데 매번 특정한 한 유저 계정으로 접속이 되어 살짝 무서운(?) 상황이 연출되었다.
이 이슈의 짧은 해결 과정을 기록해두려 한다.
원인
현재 클라이언트는 서버로부터 access 토큰과 refresh 토큰을 발급받고,
시간이 지나 토큰이 만료되면 서버에 저장된 토큰 데이터를 이용해 재발급받는 구조이다.
테스트 결과, access token이 만료되어 재발급하는 과정에서
DTO의 refresh token을 전달하는 필드에 문제가 있었다.
클라이언트에서 전달한 값이 해당 필드에 제대로 담기지 않아 이후 로직에 문제가 발생한 것이다.
async findByRefreshToken(refreshToken: string): Promise<RefreshToken | null> {
return await this.findOne({ where: { refresh_token: refreshToken } });
}의문점은 이것이었다.
왜 에러가 나는 것도 아니고, 이상한 유저의 데이터가 반환되는 것일까?
만약 에러가 발생했다면 미리 문제를 인지할 수 있었을 텐데,
다른 유저 데이터로라도 접속 자체는 되었고 에러 로그도 발생하지 않아 문제를 뒤늦게 알아차렸다.
심지어 하필 접속되는 다른 유저가 바로 내 계정이라,
테스트할 때는 정상 작동하는 것처럼 보였다.
문제의 원인은 조금만 검색해보면 쉽게 확인할 수 있었다.
TypeORM은 findOne() 메서드에서 조건값이 undefined일 경우,
해당 조건을 쿼리에서 제외하는 특징이 있었다.
따라서 where 절 자체가 무효화되고,
데이터베이스 상의 첫 번째 레코드, 즉 내 계정 데이터가 조회된 것이다.
재미있게도 나와 비슷한 경험을 한 개발자들이 여럿 있었다.
개인적으로는, 의도적으로 조건값을 undefined로 만들어 where 절을 무효화하는 경우는 드물다고 생각한다.
TypeORM이 유연한 쿼리 구성을 위해 이러한 동작을 지원하는 것은 이해되지만,
그럼에도 undefined 값이 들어올 경우 에러를 반환하도록 하는 것이 더 일반적이고 안전한 설계가 아닐까 하는 생각이 들었다.
해결 과정
문제 해결 자체는 어렵지 않았다.
DTO의 필드명이 맞지 않아 값을 제대로 전달받지 못하는 것이 근본적인 원인이었기 때문에,
해당 부분을 수정하자 바로 해결되었다.
또한 추가로 이런 상황을 방지하기 위해,
조건절에 사용하는 값이 정의되어 있는지 검사하고,
의도적으로 undefined를 허용하는 경우가 아니라면 명시적으로 에러를 발생시키도록 수정했다.
추가적으로 find() 메서드에 take 옵션을 사용해 1개만 조회하는 방식도 고려할 수 있다.
이 경우 where 절이 비어 있을 때 첫 번째 레코드를 반환하는 것이 아니라 빈 배열을 반환한다.
마무리
다행히 해당 문제는 배포 전에 발견하여 큰 문제로 이어지지 않았다.
만약 다른 유저의 데이터에 접근하는 상황이 발생했다면,
심각한 사고로 이어질 수도 있었다.
이번 문제는 단순히 운이 나빴다고 볼 수도 있지만,
그보다 더 큰 원인은 안일함이었다고 생각한다.
자주 사용하던 findOne() 메서드라 잘 알고 있다고 생각했지만,
결국 내가 모르는 동작이 있었고, 그것이 문제로 이어질 뻔했다.
앞으로는 기술을 사용할 때 단순히 사용하는 데 그치지 않고,
동작 방식까지 이해하고 사용하는 습관을 가져야겠다는 생각이 들었다.
