출시 전날 밤, 모든 문이 활짝 열려 있었다
요약
개발자가 편의를 위해 구현한 인증 우회(fallback) 기능이 심각한 보안 취약점(IDOR)을 야기했습니다. AI 파트너에게 서버 전체 검토를 요청했음에도 불구하고, 개발자 본인이 이를 병합하는 과정에서 백도어가 발생할 뻔했습니다. 결국 토큰 강제 적용 및 모든 리소스에 대한 소유권 확인 로직을 추가하여 보안을 강화했습니다.
핵심 포인트
- 편의 기능(fallback)이 심각한 보안 취약점(IDOR)을 초래할 수 있습니다.
- AI를 활용한 코드 리뷰는 유용하지만, 최종적인 책임과 검증은 개발자에게 남아있습니다.
- 인증 강제 적용 및 서버 측 소유권 확인 로직 구현이 필수적입니다.
지난 글의 마지막에서 청구서(bill)에 대해 언급했습니다. 이 글이 바로 그 청구서입니다.
커밋 로그 세 줄로 시작해 보겠습니다.
06/28 13:18 feat: enforce auth (401) — no token, no entry
06/28 13:41 fix: resource ownership checks (IDOR) — check items, body metrics, templates
06/28 23:04 submitted to App Store review
저는 모든 문을 잠갔고, 10시간 후에 제출 버튼을 눌렀습니다. 이 글은 애초에 그 문들이 어떻게 열려 있게 되었는지—그리고 누가 그것들을 발견했는지에 대한 내용입니다.
백도어는 어떻게 탄생했나
첫 이틀로 돌아가 봅시다. 앱도, 로그인 화면도 없었고, 서버 API를 테스트한다는 것은 토큰을 다루는 것을 의미했습니다. 짜증스러웠습니다. 그래서 저는 편의 기능을 추가했습니다: 쿼리 문자열에 ?userId=1만 넣으면 그냥 그 사용자가 되는 폴백(fallback) 기능입니다. 토큰이 필요 없습니다. 개발자에게는 천국이죠. 한 줄의 curl 명령으로 모든 것이 작동합니다.
이것은 교과서적인 '지금 바로 작동하는' 코드입니다. 그리고 지난 글에서 말했듯이, 이런 종류의 코드는 데모에서는 절대 나타나지 않습니다. 모든 기능이 작동하죠. 집은 문을 열어둔 채로도 완벽하게 좋아 보입니다.
6일 전의 저는 기능 목록에 취해 문이 존재한다는 사실을 잊고 있었습니다.
빌더에게 건물 검사를 요청했다
제출 전 주말, 저는 기능 추가를 멈추고 제 AI 파트너에게 다른 임무를 맡겼습니다:
"실제 사용자들이 이제 이것을 만질 것이다. 보안 관점—인증(auth), 권한(permissions), 데이터 노출까지 서버 전체를 검토해라. 공격자처럼 생각하라."
일주일 내내 벽돌을 쌓던 바로 그 파트너가 검사관의 모자를 쓰고 자기 자신의 벽을 두드리기 시작했습니다. 몇 번 돌아간 후, 보고서가 도착했습니다.
요약: 어떤 토큰도 없이, 단지 ?userId=만 변경해서 누구나 모든 사용자의 데이터를 읽고 수정할 수 있었습니다. 식사 기록, 운동량, 체중—모든 것이었습니다. 서버에는 인증이 없었습니다. 인증처럼 보이는 장식품만 있을 뿐이었습니다.
하지만 저를 정말 소름 돋게 만든 부분은 그 문이 AI의 실수였다는 것이 아니었습니다. 제가 요청했고, 검토했으며, 병합한 것이었기 때문입니다. 만약 이것이 회사 코드였다면, 9년 동안 쌓인 습관이 리뷰 과정에서 즉시 이를 감지했을 것입니다. 하지만 그것은 제 프로젝트였기에 '나중에 제거해도 돼'라는 생각이 괜찮게 느껴졌습니다. 속도 요금 청구서는 이렇습니다: 출시 직전에 이자까지 붙어서 도착합니다.
그날 밤
저는 그날 밤에 바로 고쳤습니다.
이 정책은 의도적으로 단호했습니다. 왜냐하면 모든 예외는 미래의 백도어(backdoor)가 되기 때문입니다:
- 토큰 없음 → 401. 어디서든. 폴백(fallbacks)이 없습니다. 화이트리스트는 로그인과 회원가입만 가능하며, 그 외에는 아무것도 아닙니다.
- 모든 리소스에 대한 소유권 확인. '이 검사 항목이 실제로 이 토큰 소유자에게 속하는가?' — 매번 서버 측에서 검증됩니다. 이것은 또한 다른 사람의 ID를 사용한 IDOR(Insecure Direct Object Reference) 탐색도 막아줍니다.
- 모든 권한 부여 결정은 서버에 존재합니다. 클라이언트에서만 무언가를 차단하는 것은 아예 차단하지 않는 것과 같습니다.
저는 새벽까지 이 코드를 수정했고, 커밋들은 일요일 오후에 올라왔습니다 — 13:18 인증 강제 적용, 13:41 소유권 확인 — 그리고 그날 밤 23:04에 리뷰를 요청했습니다.
유일한 자비는 이것이었습니다. 그때 앱에는 정확히 한 명의 사용자, 저 자신만 있었습니다. 열린 문을 지나다닐 사람이 아무도 없었죠. 그래서 피해는 제로였지만 — 솔직히 말해서, 그것은 실력이 아닙니다. 운입니다. 만약 제가 모른 채 출시했고, 실제 사용자를 확보했으며, 그 후에 누군가 이것을 발견했다면? 이 시리즈는 빌드 로그(build log)가 아닐 것입니다. 사과문이 될 것입니다.
저에게 남은 규칙
모든 '바이브 코딩(vibe-coding)' 대화는 결국 같은 걱정에 도달합니다:
모순적으로 들리지만 핵심은 역할입니다. 벽돌을 쌓는 AI와 검사를 수행하는 AI는 같은 코드를 다른 시각으로 바라봅니다. 만약 여러분이 항상 '이것을 구축하라'고만 말하고 결코 '이제 공격해 보라'고 말하지 않는다면, 두 번째 시선은 절대 열리지 않습니다. 그리고 무엇을 요청해야 하는지 아는 것—애초에 '보안 검토(security review)'가 출시 전 체크리스트에 포함되어야 한다는 것을 아는 것은 도구에서 나오는 것이 아닙니다. 그것은 다른 사람들의 서버가 불타오르는 것을 9년 동안 지켜본 경험에서 나옵니다.
AI로부터 속도를 얻고, 책임감은 저에게 있습니다. 세 번째 게시물부터 이 문장은 더 이상 슬로건이 아닙니다. 잠 못 이루는 밤을 보낸 남자가 쓰는 말입니다.
다음 게시물에서는 분위기를 바꿔서 제가 직접 만든 앱으로 제 자신의 컷(cut)을 거치며 테스트해 보겠습니다. 다룰 내용은 다음과 같습니다: 데이터를 저장했지만 비어 있는 목록을 보여준 미스터리 (원인: 9년차 개발자도 당하는 JPA 트랩), 그리고 화면에 고정된 숫자가 아예 아무 숫자도 없는 것보다 더 나쁘다는 것을 깨달은 날입니다.
Baanbok은 App Store에서 무료로 이용 가능합니다—가입할 필요 없이, 게스트 모드로 모든 기능을 사용할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기