당신의 주간 보안 점검 메일은, 실제로 제일 위험한 항목을 보여줍니까?
Supabase 프로젝트 하나에서 주간 보안 메일이 왔습니다. 내용은 딱 하나 — RLS가 꺼진 테이블 1건. 별로 급해 보이지 않아서 미루려다, 이번엔 메일 대신 Management API로 advisor 결과를 전량 받아봤습니다. 숫자가 완전히 달랐습니다. ERROR 1건, WARN 227건, INFO 19건. 메일은 ERROR 1건만 알려줬고, 진짜 구멍은 메일에 한 번도 언급되지 않은 WARN 더미 안에 있었습니다.
당신이라면?
주간 요약 메일이 이슈 1건이라고 말할 때, 그걸 믿고 넘어가시겠습니까, 아니면 API로 전량을 받아 직접 세어 보시겠습니까? 저는 8월 26일 전까지 전자였습니다.
227건 중에 뭐가 있었나
WARN 더미를 걸러보니 security definer로 도는 함수 중 anon 키만으로 실행 가능한 게 106건, 그중 아무 검사도 없는 무방비 함수가 23건이었습니다. 최악 두 개는 크레딧을 임의 uuid에 무제한 지급하는 함수와 구독을 임의로 활성화하는 함수였습니다. 앱 코드를 전수 grep해서 두 함수 모두 호출부가 0건(전부 Edge Function이나 service_role 경유)인 걸 확인하고 EXECUTE를 회수했습니다. 여기까지도 메일 한 줄로는 절대 안 나오는 내용이었습니다.
그리고 게시판 앱의 삭제 함수 두 개, web_post_delete와 web_comment_delete를 열어봤습니다.
함수 안에 박힌 진짜 마스터키
두 함수 본문 안에는 운영자 토큰의 sha256 값이 상수로 박혀 있었습니다.
if sha256(input_token) = '9f2a...(상수)' then
delete from posts where id = post_id; -- 작성자 검사 없이 통과
end if;그리고 이 함수들엔 anon EXECUTE가 걸려 있었습니다. 문제는 이 anon 키가 비밀이 아니라는 겁니다 — iOS Info.plist와 Android BuildConfig 양쪽에 그대로 박혀서 앱을 뜯으면 누구나 꺼낼 수 있습니다. 즉 저 상수 해시 하나만 같이 알면, Supabase REST를 직접 호출해서 아무 글이나 지울 수 있었습니다. 실제로 그 상수를 그대로 넣어 REST 호출을 재현해봤고, http 200이 돌아왔습니다. 삭제됐습니다.
운영자 삭제 기능이 필요해서 만들었을 텐데, 정작 지금 이 기능을 쓰는 운영 화면조차 없었습니다. 켜 놓기만 하고 아무도 안 쓰는 관리자 뒷문이, 앱 바이너리 안에 공개된 열쇠로 열려 있었던 셈입니다.
왜 이게 더 위험한가
크레딧 무제한 지급은 발견하기 쉽습니다. 함수 이름부터 grant_credits고, 인자로 uuid를 받는 순간 "이거 아무나 부르면 큰일 나겠는데"가 바로 보입니다. 하지만 삭제 함수 안에 박힌 상수 하나는 다릅니다 — 함수 시그니처만 보면 평범한 "토큰 검사 후 삭제"입니다. 위험이 코드 형태가 아니라 상수 값 자체에 있어서, 시그니처 리뷰로는 안 잡히고 본문을 열어 상수를 눈으로 확인해야만 보입니다. advisor도 "anon이 이 함수를 실행할 수 있다"까지만 알려주지, 그 함수 안에 뭐가 박혀 있는지는 안 알려줍니다.
수정
패치가 아니라 삭제였습니다. 운영자 삭제 분기를 통째로 들어내고, 삭제를 작성자 토큰 검사 하나로 통일했습니다(글 수정 함수와 같은 형태). 상수도, 그 상수를 검사하는 분기도 이제 코드에 없습니다. 있는 걸 더 막는 것보다, 없어도 되는 뒷문을 없애는 쪽이 더 짧고 더 안전했습니다.
한 가지 더: create or replace function은 기존 권한(ACL)을 그대로 유지합니다. "새로 정의했으니 권한도 초기화됐겠지"는 틀린 가정입니다. 권한이 기본값으로 돌아가는 건 drop 후 재생성이거나 시그니처를 바꿀 때뿐이라, 이번처럼 본문만 고치는 수정에서는 anon EXECUTE를 명시적으로 REVOKE하지 않으면 뒷문이 함수 이름과 함께 그대로 살아남습니다.
자가진단 3개
- 벤더가 보내는 보안 요약 메일과, 그 벤더 API로 직접 받은 전량 목록을 대조해본 적 있습니까? 요약은 벤더가 고른 것만 보여줍니다.
- anon/공개 키로 실행 가능한 함수 본문 안에, 상수로 박힌 토큰·해시·비밀번호가 있는지 grep해보셨습니까?
- 클라이언트 바이너리에 박힌 키를 "사실상 비공개"라고 가정하고 권한을 설계한 부분이 남아있지 않습니까?
검증 리허설이 실사용자 데이터를 건드리고 한 컬럼만 되돌린 이야기는 이 같은 보안 점검 세션에서 나온 다른 함정입니다 — 하나는 "권한이 너무 넓게 열려 있었다", 다른 하나는 "그걸 고치던 검증 절차가 또 다른 구멍을 냈다"는 점에서 짝을 이룹니다.
이 글에는 후속편이 있습니다. 회수했던 실행 권한이 어느 재생성에서 조용히 되돌아와 있던 이야기입니다. 여기서는 함수 본문에 박힌 상수가 문제였고, 후속편은 함수 권한 자체가 되돌아온 경우입니다.
지금 anon 키로 실행 가능한 함수 목록을 한 번 뽑아, 본문 안에 상수가 박힌 게 있는지 눈으로 확인해 보세요. 시그니처 리뷰로는 안 잡힙니다.