노출된 .env 파일과 유출된 키
노출된 .env 파일이란 데이터베이스 비밀번호, 메일 자격 증명, API 키를 담고 있는 구성 파일이, 이름을 대는 누구에게나 웹 서버가 건네주는 상태이며, 노출된 .git 폴더는 전체 소스 기록이 붙은 같은 사고입니다. 어느 쪽도 공격자가 무언가를 깨뜨릴 필요가 없습니다. 파일이 그저 서버가 게시하는 장소에 놓여 있을 뿐이며, 평범한 GET 요청 한 번으로 다운로드됩니다. 이 글은 그것이 어떻게 발생하는지 — 문서 루트 실수, 저장소 자체를 제공하는 정적 호스트, 아무도 감사하지 않은 빌드 산출물 —, 발견자가 첫 한 시간에 키로 무엇을 하는지, 그리고 나중에 파일을 삭제하는 것이 대응에서 가장 작은 부분인 이유를 설명합니다. 지금 자신의 사이트를 확인하려면 유출된 비밀 스캐너를 실행하고 결과와 함께 읽어 보세요.
개인 파일이 공개되는 방법: 문서 루트
모든 웹 서버에는 문서 루트가 있습니다. 그 내용이 정의상 공개인 디렉터리입니다. 그 안에 들어간 것은 — 의도적이든 우연이든 — 어떤 페이지가 링크하든 안 하든 URL로 요청할 수 있습니다. 고전적인 사고는 작업 디렉터리 전체를 서버에 복사해 배포하는 프로젝트입니다. 원래 있어야 할 애플리케이션 코드는 그 옆의 .env 파일, 그 안의 .git 폴더, 편집기 백업 사본, 마이그레이션 전에 누군가 만든 데이터베이스 덤프, 지난달 버전의 아카이브와 함께 도착합니다. 서버는 이것들을 구별할 수 없습니다. 서버는 파일을 제공할 뿐이며, 개인적인 것들도 파일입니다.
프레임워크 관례는 함정에 빠지기 쉽게 만듭니다. .env 파일은 문서 루트보다 한 단계 위에, 혹은 배포되는 트리 완전히 밖에 있어야 하며, 애플리케이션은 웹 서버를 통해서가 아니라 디스크에서 읽어야 합니다. 그러나 개발 서버는 기꺼이 프로젝트 루트를 제공하고, 작은 배포는 모든 것을 public_html로 끌고 가며, 튜토리얼에서 복사한 구성은 문서 루트를 프로젝트 자체로 향하게 할 수 있습니다. 이런 일이 일어나도 눈에 띄게 깨지는 것은 없으며, 바로 그것이 문제입니다. 사이트는 완벽히 작동하고 파일은 공개되어 있으며, 가장 먼저 알아차리는 사람은 /.env를 요청한 사람 — 자동화 스캐너가 인터넷의 모든 사이트에 끊임없이 보내는 바로 그 요청 — 입니다.
정적 호스트, 제공되는 저장소, 빌드 산출물
정적 호스팅은 서버 구성을 방정식에서 빼고 더 단순한 규칙으로 바꿉니다. 게시 디렉터리에 있는 것이 게시된다는 규칙입니다. 출력 디렉터리를 프로젝트 루트로 정해 저장소를 정적 호스트에 배포하면, 호스트는 저장소를 충실히 제공합니다 — 출력에 복사되었다면 .git도 포함됩니다. 노출된 .git은 보이는 것보다 나쁩니다. HEAD 파일과 구성은 구조를 증명하고, 객체 파일에는 커밋된 모든 blob이 들어 있으며, 평범한 Git 도구로 전체 기록을 재구성할 수 있습니다 — 한 번 커밋되고 나중 커밋에서 "삭제"된 비밀도 포함되며, 그 삭제는 작업 트리에서만 제거할 뿐 그 외 어디에서도 제거하지 않습니다.
빌드 산출물은 두 번째, 더 조용한 노출 부류를 만듭니다. JavaScript 번들러는 코드가 참조하는 것을 인라인하므로, 빌드 시점에 환경 변수에서 읽힌 API 키가 제공되는 번들 안의 리터럴 문자열로 끝날 수 있습니다 — 설계상 공개이고 모든 방문자가 읽을 수 있으며, 그 파일은 원래 공개되어야 했기에 어떤 권한 검사에도 걸리지 않습니다. 소스 맵은 원본 소스를 번들 옆에 실어 상황을 악화시킵니다. 이 중 어느 것도 잘못 구성된 서버를 필요로 하지 않습니다. 필요한 것은 외부에서 한 번도 검사되지 않은 빌드뿐입니다.
세 번째 패턴은 남은 찌꺼기입니다. 아카이브를 웹 루트에 풀고 그 아카이브를 거기에 남겨 두는 배포 스크립트, 웹으로 접근 가능한 폴더에 덤프를 쓰는 백업 플러그인, 편집기가 저장한 config.php.bak — 많은 서버는 확장자가 더 이상 PHP 핸들러와 일치하지 않기 때문에 정확히 그것을 평문으로 제공합니다. 각 찌꺼기는 예측 가능한 주소에 있는, 사적인 것의 완전한 사본입니다.
공격자가 첫 한 시간에 하는 일
발견은 자동화되어 있고 끊임없습니다. 스캐너는 거대한 주소 범위에 걸쳐 알려진 경로 — env 파일, Git 내부, 구성 이름, 백업 확장자 — 를 열거하며, 새로운 노출은 보통 몇 시간 안에, 종종 더 빨리 발견됩니다. 서버에 도착하는 것은 200을 돌려주는 평범한 GET 요청입니다. 서버 관점에서는 이상한 일이 아무것도 일어나지 않았으므로 경보는 없습니다. 따라서 하루라도 공개되었던 파일은 다운로드되었다고 가정하고, 입증 책임은 반대 방향으로 흐른다고 생각하세요.
선별도 자동화되어 있습니다. 다운로드된 .env는 몇 초 만에 파싱됩니다. 데이터베이스 자격 증명은 닿을 수 있는 모든 데이터베이스 포트에 시험되고, 클라우드 키는 살아 있는지 알려 주는 무해한 API 호출로 검증되며, 메일 자격 증명은 발송에 시험되고, 결제 키는 모드가 확인됩니다 — 라이브 모드 결제 키와 프로덕션 데이터베이스 자격 증명은 대박이며, 각각이 곧바로 돈이나 데이터로 바뀌기 때문입니다. 유효한 키는 소유자가 언제든 교체할 수 있으므로 빠르게 사용되고, 거래되고, 보유됩니다. 노출된 키가 가장 가치 있는 때는 첫 한 시간이며, 그 시간은 효율적으로 쓰입니다.
파일을 삭제하는 것만으로 부족한 이유
파일을 삭제하면 새 다운로드는 멈춥니다. 이미 존재하는 사본에는 아무 효과가 없습니다. 발견자의 사본은 물론이고, 당신이 직접 만든 사본도 있습니다 — CDN이나 회사 프록시에 캐시된 응답, 검색 엔진이나 아카이브 서비스의 크롤링 속 스냅샷, 브라우저가 이미 다운로드한 번들입니다. 모든 캐시가 비워지고 만료될 때까지, "삭제된" 파일은 당신이 통제하지만 청소를 잊은 인프라에서 계속 제공될 수 있으며, 통제 밖에 있는 버전은 영원히 그 밖에 있습니다.
그래서 확인된 노출에 대한 첫 대응은 삭제가 아니라 교체입니다. 노출된 자격 증명을 폐기하고 새것을 발급하면 전 세계의 모든 사본이 같은 순간에 무가치해집니다. 교체에는 순서가 있습니다 — 대체품을 발급하고, 배포하고, 서비스가 작동하는지 확인한 뒤, 예전 자격 증명을 폐기합니다 — 그리고 노출된 자료가 열 수 있는 모든 것에 미칩니다. .git 노출은 현재의 것뿐 아니라 기록에 나타난 모든 비밀을 교체한다는 뜻입니다. 공개된 것이 기록 자체이기 때문입니다. 그 뒤에는 노출된 경로에 대한 액세스 로그를 읽어 보세요. 당신이 가장 먼저 알았는지 알려 주며, 더 이른 가져오기는 구성 사고를 전혀 다른 점검 목록을 가진 침해 조사로 바꿉니다.
탐색 목록 해설 — 그리고 예방 점검 목록
유출된 비밀 스캐너 같은 스캐너는 파일을 이름으로만 요청할 수 있으므로, 그 목록은 알려진 집합입니다. /.env와 그 환경 변형, /.git/HEAD와 /.git/config, 익숙한 구성 파일 이름, 백업 및 아카이브 확장자, 키 파일 이름 — 그리고 페이지 자체의 스크립트를 키 모양의 문자열로 검색합니다. 정직한 한계는 양방향으로 작용합니다. 스캐너는 요청한 것만 찾으므로, 발견은 상태 코드가 아니라 내용으로 확인합니다. 홈페이지를 돌려주는 200은 포괄 경로이지 유출이 아니며, 그것을 유출로 세는 보고서는 스스로를 부풀리는 것입니다. 관련된 공급망 위험 — 다른 곳에서 의도적으로 불러오는 스크립트 — 은 타사 스크립트와 공급망 위험에서 다룹니다.
예방은 주로 배치와 거부 한 겹입니다. 비밀은 문서 루트 밖, 저장소의 게시 출력 밖에 두세요. 위험한 이름이 거기에 떨어져도 서버가 거부하게 하세요 — 점 파일, env 및 구성 확장자, 아카이브, 키 자료에 대한 거부 규칙을 실제로 요청해 시험합니다 — 배포 산출물에서 .git을 제거하거나 단호히 차단하세요. 파이프라인에서는 CI에 비밀 스캔을 추가하세요 — 커밋 시에는 키가 출하되기 전에 멈추도록, 빌드 산출물에는 번들된 키가 낯선 사람이 아니라 출력 안에서 잡히도록요. 마지막으로, 일정에 따라, 그리고 배포가 바뀔 때마다 직접 스캔하세요. 이 글의 노출은 모두 소리 없으며, 남보다 먼저 자신의 노출을 아는 유일한 방법은 스캐너가 묻는 것과 같은 질문을 자신의 서버에 묻는 것입니다.