Skip to content
[012]2026.05.28log

열려 있던 문

론칭 전 보안 점검에서 발견한 것

깨달음: "혼자 만드는 집에서는, 문단속도 혼자다. 그리고 문은 잠그기 전까지 열려 있다."


론칭이 가까워지면서 한 가지가 계속 마음에 걸렸다. 보안이다.

회사라면 보안팀이 있다. 코드를 검토하는 동료가 있고, 출시 전에 침투 테스트라는 것도 한다. 나에게는 그런 것이 없다. 코드를 읽지 못하는 내가 보안 구멍을 눈으로 찾을 가능성은, 없다고 보는 게 정직하다.

그래서 AI에게 시켰다. 역할을 정해줬다. "당신은 시니어 보안 검토자다. 결제, 인증, 데이터 접근 — 전부 의심하고 전부 확인해라."

검토 결과가 돌아왔다. 결제 검증은 견고하다. 암호화도 정석이다. 인증도 서버에서 제대로 확인하고 있다. 여기까지 읽으면서 안심했다. 그리고 다음 줄에서 안심이 끝났다.

치명적 문제 1건. 데이터베이스 함수 8개가, 누구에게나 열려 있습니다.

설명은 이랬다. 데이터베이스에는 "이 사용자의 노트 목록을 꺼내줘" 같은 일을 하는 함수들이 있다. 그 함수들에 사용자 ID를 넣으면 그 사람의 데이터가 나온다. 문제는 이 함수를 호출할 수 있는 권한이었다. 원래는 서버만 호출할 수 있어야 한다. 그런데 데이터베이스는 함수를 만들면 기본값으로 모든 사람에게 실행 권한을 준다. 나는 그 기본값을 몰랐고, 그래서 바꾸지 않았다.

결과적으로, 로그인조차 하지 않은 사람이 임의의 사용자 ID를 넣어 호출하면 그 사람의 노트와 주제와 전자책 목록이 나오는 상태였다. 실제로 되는지 확인해봤다. 됐다. 문은 진짜로 열려 있었다.

등에서 식은땀이 났다. 사용자의 데이터를 지켜주겠다고 암호화니 뭐니 해놓고, 뒷문이 열려 있었던 것이다. 아무도 그 문으로 들어오지 않은 것은 전적으로 운이었다. 아직 서비스가 알려지지 않았다는 운.

수정 자체는 하루가 걸리지 않았다. 함수 8개의 권한을 회수하고, 서버만 호출할 수 있게 잠갔다. 잠긴 것을 세 번 확인했다.

그날 알게 된 것이 두 가지 있다.

하나. 기본값은 설계가 아니다. 데이터베이스가 문을 열어두는 게 기본값이라면, 잠그는 것은 내 일이다. "몰랐다"는 변명은 내 집에서는 통하지 않는다. 내가 모르면 아무도 모르는 채로 서비스가 굴러간다.

둘. 같은 실수는 체크리스트가 돼야 한다. 이 8개의 함수는 몇 달에 걸쳐 하나씩 만들어졌고, 전부 같은 실수를 반복하고 있었다. 첫 번째 함수에서 잡았다면 나머지 일곱 개는 없었을 것이다. 그래서 개발 규칙 문서에 한 줄을 추가했다. 이런 함수를 만들 때는 반드시 권한부터 회수할 것. 다음에 만들 함수는 이 체크리스트를 통과해야 태어난다.

보안은 기능이 아니라서 화면에 보이지 않는다. 잘해도 아무 일도 일어나지 않는 것이 보안이다. 아무 일도 일어나지 않는 상태를 만들기 위해 하루를 썼고, 그 하루는 이 여정에서 가장 잘 쓴 하루 중 하나였다고 생각한다.


🔧 이 에피소드의 기술 용어 해설

IDOR (Insecure Direct Object Reference) 다른 사람의 데이터를 가리키는 ID를 바꿔 넣으면 그대로 열람되는 취약점. "내 주문번호 1001을 1002로 바꿨더니 남의 주문이 보인다"가 전형적인 예.

RPC (Remote Procedure Call) 데이터베이스나 서버에 미리 만들어둔 함수를 밖에서 호출하는 방식. 이번 사건의 무대.

REVOKE / GRANT 데이터베이스 권한을 회수하고(REVOKE) 부여하는(GRANT) 명령. 이번 수정의 핵심은 "모두에게서 회수하고, 서버에게만 부여"였다.

침투 테스트 (Penetration Test) 공격자의 입장에서 실제로 뚫리는지 시험해보는 보안 점검. 이번에는 AI가 그 역할을 했고, 실제로 뚫렸다.