초록불을 믿으면 안 되는 순간들
배포 전에 점검을 돌리면 초록불이 뜹니다. 테스트도 통과, 검사도 통과. 그걸 보고 안심하고 올립니다.
그런데 지난 몇 달 동안 저를 가장 놀라게 한 일들은 전부 그 초록불 뒤에 숨어 있었습니다. 점검이 실패해서 발견한 게 아니라, 점검이 통과하고 있었는데 사실은 아무것도 확인하지 않고 있었던 경우들입니다.
테스트 한 번에 운영 데이터가 날아갈 뻔했다
가장 아찔했던 건 이겁니다.
테스트를 돌리면 매번 테이블을 전부 비우고 시작하도록 되어 있었습니다. 테스트끼리 데이터가 섞이지 않게 하려는 아주 흔한 장치입니다. 그런데 어느 데이터베이스를 비우는지는 아무도 확인하지 않았습니다. 설정 예시 파일의 기본값이 운영에서 쓰는 이름과 같았습니다.
즉, 설정 파일을 제대로 안 만든 상태에서 테스트를 한 번 실행하면 운영 데이터가 통째로 지워질 수 있었습니다. 실제로 터지지는 않았지만 터질 수 있는 상태로 몇 주가 지나 있었습니다.
막는 방법은 어렵지 않았습니다. 테스트가 시작되기 전에 접속할 데이터베이스 이름을 확인하고 개발용 이름 규칙에 맞지 않으면 아예 실행을 거부하게 했습니다. 중요한 건 이 검사를 어디에 두느냐였습니다. 테스트 준비 단계에 두면 이미 첫 번째 삭제가 나간 뒤일 수 있습니다. 그래서 테스트를 수집하기도 전, 가장 앞 단계에서 막도록 했습니다.
비밀번호가 주석에 적혀 있었다
설정 코드에 긴 주석이 하나 있었습니다. "접속 주소를 문자열로 직접 조립하면 특수문자에서 깨진다"는 교훈을 설명하는 내용이었고, 이해를 돕겠다고 예시를 붙여 놨습니다.
그 예시가 실제 계정과 실제 비밀번호였습니다. 운영 설정 파일의 값과 글자 하나까지 같았습니다. 형상관리 이력에도, 배포된 서버에도, 만들어진 이미지에도 그대로 들어가 있었습니다.
더 뼈아픈 건 그 두 주 전에 보안 점검을 돌렸는데 그 점검이 "하드코딩된 비밀번호 0건"으로 통과시켰다는 사실입니다. 왜 못 잡았느냐면 그 검사가 변수에 값을 넣는 문장만 보고 있었기 때문입니다. 주석은 검사 대상이 아니었습니다.
같은 시기에 비슷한 것이 하나 더 나왔습니다. 설정 화면을 찍은 캡처 이미지가 저장소에 들어가 있었고 거기에 인증 정보가 그대로 읽혔습니다. 코드만 훑는 검사로는 절대 안 나옵니다. 스크린샷도 소스라는 걸 그때 알았습니다.
지금은 주석에 쓰는 예시 값을 누가 봐도 가짜인 문자열로만 씁니다. 사람의 주의력에 기대면 언젠가 놓치므로, 실제 값이 다시 들어오면 테스트가 잡아내게 해 뒀습니다.
코드에는 아무 문제가 없었다
암호화되지 않은 접속이 열려 있었던 적도 있습니다. 주소 앞에 http만 붙여 열면 그대로 응답했고 그 상태로 로그인 요청까지 통했습니다. 로그인 요청이 한 번만 평문으로 나가면 인증 정보가 통째로 샙니다.
이건 코드를 아무리 읽어도 안 나옵니다. 코드에는 정말 문제가 없었습니다. 앞단 설정과 도메인 서비스 설정 양쪽에 평문 접속을 막는 항목이 없었을 뿐입니다.
그래서 요즘은 배포 후 점검에 "주소를 실제로 한 번 불러 보기"를 넣어 뒀습니다. 코드 검사로 찾을 수 있는 문제와 실제로 요청을 보내야만 드러나는 문제는 종류가 다릅니다.
막으려고 만든 것이 공격 수단이 되다
과도한 요청을 막는 장치를 붙였는데, 그 장치 자체에 문제가 두 가지 있었습니다.
첫째, 누가 보냈는지 판단하는 기준을 요청자가 스스로 정할 수 있었습니다. 요청에 붙는 접속자 정보 항목은 중간 경유지들이 계속 덧붙이는 구조인데, 그중 맨 앞이 방문자가 직접 적어 보낸 글자였습니다. 그걸 읽고 있었으니 매번 다른 값을 적어 보내면 제한이 걸리지 않았습니다.
더 나빴던 건 반대 방향입니다. 남의 접속 정보를 적어 보내면 그 사람의 로그인을 대신 잠글 수 있었습니다. 우회에 그치지 않고 공격 도구가 되는 상태였습니다. 중간 서비스가 덮어써 주는 값을 우선해서 보고 없으면 뒤에서부터 훑어 판단하도록 바꿨습니다.
둘째, 기록 공간이 꽉 차면 그다음 요청을 검사 없이 통과시키고 있었습니다. 안전하게 열어 두는 쪽을 택한 건데, 그 공간을 60초 안에 채울 방법이 있었습니다. 채우는 동안은 모든 사람의 제한이 함께 사라집니다. 오래된 기록부터 밀어내도록 고쳤습니다. 전부 막아 버리는 쪽으로 만들지 않은 건, 그러면 서비스가 멈추고 그게 공격자가 원하는 결과이기 때문입니다.
통과하면서 아무것도 확인하지 않는 테스트
같은 장치를 켰을 때 벌어진 일이 더 인상적이었습니다.
요청 제한을 켜자 기존 테스트 몇 개가 깨졌습니다. 그건 바로 고쳤습니다. 문제는 깨지지 않은 하나였습니다. 그 테스트는 특정 상황에서 세션이 끊기는지를 확인하는 것이었는데, 제한에 걸려 요청이 막히는 바람에 끊기는 상황 자체가 만들어지지 않았습니다. 그런데도 마지막 확인 문장은 성립해서 통과했습니다.
깨진 테스트는 눈에 띄지만 초록불인데 속이 비어 있는 테스트는 아무도 모릅니다. 그 뒤로 새 방어 장치를 넣을 때는 그 장치가 가로채는 기존 테스트가 무엇인지 먼저 세어 봅니다.
검사 도구의 버전도 검사 대상이다
구조 검증 명령이 운영에서는 계속 실패인데 제 컴퓨터에서는 통과로 나오는 일이 있었습니다. 한참을 코드에서 원인을 찾다가 알았습니다. 제 컴퓨터에 깔린 도구 버전이 낮아서, 문제가 되는 항목을 비교하는 기능 자체가 없었던 겁니다.
같은 명령이 같은 대상을 보고 있었지만 초록불의 뜻이 서로 달랐습니다. 이후로는 검증 도구도 고정된 버전을 쓰도록 맞췄습니다.
없는 척하는 게 나을 때
설계 쪽 이야기도 하나 덧붙입니다.
남의 데이터에 접근하면 보통 "권한이 없습니다"를 돌려주고 싶어집니다. 그쪽이 친절합니다. 그런데 이 응답은 "그 번호는 존재한다"는 정보를 같이 알려 줍니다. 번호를 1부터 쭉 훑으면 몇 개가 있는지 셀 수 있습니다. 그래서 남의 것에 접근하면 "없습니다"로 응답하도록 통일했습니다.
같은 이유로 로그인 없이 열리는 목록 화면에서는 내려보내는 항목을 최소한으로 줄였습니다. 이메일이나 참여 코드 같은 건 당연히 빼고 영상 미리보기 이미지도 뺐습니다. 미리보기 이미지 주소에 영상 식별자가 그대로 들어 있어서, 이미지를 보여 주는 순간 주소가 공개되는 셈이었기 때문입니다. 이건 응답 본문 전체를 글자로 훑어 보는 테스트가 잡아냈습니다. 사람 눈으로는 못 찾았습니다.
남은 생각
몇 달 동안 배운 걸 한 줄로 줄이면 이렇습니다. 점검이 통과했다는 사실보다, 그 점검이 무엇을 보고 있는지가 훨씬 중요합니다.
변수 대입만 보는 검사는 주석을 못 봅니다. 코드만 보는 검사는 캡처 이미지를 못 봅니다. 요청을 보내 보지 않는 검사는 설정 문제를 못 봅니다. 그리고 상황이 만들어지지 않은 테스트는 무엇이든 통과시킵니다.
그래서 요즘은 검사를 하나 추가할 때마다 "이건 무엇을 못 보는가"를 같이 적어 둡니다. 적어 두면 적어도 다음에 비슷한 게 터졌을 때 어디를 볼지는 압니다.