본문으로 건너뛰기
>_ITDITD웹 보안 플랫폼

보안 가이드

Laravel 앱의 .env가 온 세상에 읽혔다 — 가장 흔한 공유 호스팅 실수

공유 호스팅에서 Laravel 앱은 .env, .git, DB 덤프, OAuth 시크릿이 HTTP로 읽히는 상태가 되곤 한다. 원인: public/만이 아니라 프로젝트 전체가 웹 루트 아래 놓인 것. 세 단계 수정과, 이것이 업계 전반의 나쁜 패턴인 이유.

게시 2026-06-07 5분 읽기

공유 호스팅에서 실제로 일어나는 설정 오류를, 도메인과 값을 제거하고 교훈으로 바꾼 글이다. 이것은 남의 사이트를 찾는 이야기가 아니라 — 자기(또는 인계받은) 배포를 안전하게 하는 이야기다.

사례 파일 — INCIDENT FILE
분류
.env / .git / DB 덤프 노출(설정 오류)
심각도
크리티컬(모든 시크릿이 HTTP로 읽힘)
원인
Laravel 본체를 public_html 바로 아래 배치(public/만 노출돼야 함)
전파
새 앱마다 같은 구멍을 복사 → 수십 개
영구 조치
본체를 도큐먼트 루트 바깥에 + public/만 symlink

최악의 파일은 .env와 .git

.env는 키링이다: DB 인증, 메일, 외부 API, 앱 암호화 키(APP_KEY). .git이 공개되면, 공격자는 현재 값뿐 아니라 git 히스토리를 훑어 과거의 모든 교체 이력까지 복원한다. .env란 무엇인가 참고.

무슨 일이 일어나고 있었나

공유 호스트에서 많은 Laravel 앱이 이런 식으로 배치되어 있었다. 위험한 배치와 올바른 배치를 나란히 본다:

위험한 배치(흔함)

~/public_html/
└── app/          ← Laravel 전체(틀림)
    ├── .env      ← /app/.env 로 읽힘
    ├── .git/     ← clone으로 전체 히스토리 복원
    ├── vendor/  config/  storage/
    └── public/   ← 여기 있어야 할 유일한 것

올바른 배치

~/laravel/app/    ← 본체는 웹 루트 바깥
├── .env  .git/   ← HTTP로 닿지 않음 = 안전
└── public/       ← 이것만 symlink로 노출
        └── index.php

외부에 200 OK를 반환하던 전형적 파일들:

파일위험한 이유
.env / .env.bk_*DB 인증, 메일, 외부 API 키, APP_KEY — 전부 평문
.git/ 디렉터리git clone으로 소스와 전체 히스토리 복원
composer.lock / package-lock.json정확한 의존성 버전 = 알려진 CVE 표적 목록
credentials.jsonOAuth 클라이언트 시크릿 묶음
*.sql / db-*.sql.gz전체 데이터베이스 덤프, 압축해도 수십 MB

무서운 점: 전파

한 번 이렇게 배치하면, 같은 종류의 새 앱마다 같은 구멍을 복사한다. 설정 오류 하나가 수십 개가 된다. 철칙: 알아챈 순간, 전부 점검하라.

세 단계 수정

1단계 — 응급 처치: 민감 파일을 HTTP 수준에서 차단

먼저 "더 새는 것"을 멈춘다. 되돌릴 수 있고 정상 페이지에 영향이 없다. public_html/.htaccess 맨 위에 거부 블록을 추가하라(방어용 설정 예시):

# === SECURITY BLOCK ===
# .env / .git 아래 모든 것을 404
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
# 백업, 덤프, 자격증명 파일 거부
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
# /<app>/storage/... 등 Laravel 내부가 보이는 경우
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

하지만 블랙리스트는 근본적으로 두더지잡기다. composer.lock을 잊을 것이고; 새 파일명이 새 구멍을 만든다. 최선의 응급 처치로 여기고, 진짜 수정은 3단계에 두라.

2단계 — 키 교체(봤다고 가정)

.htaccess로 차단한 뒤에도 시크릿은 이미 봤다고 다뤄라. 우선순위 순:

  1. 외부 API 키(최우선, 즉시): 과금과 계정 탈취로 직결된다. OAuth CLIENT_SECRET, API 키, 클라우드 자격증명을 폐기 + 재발급하되 최소 권한으로 범위 한정.
  2. OAuth 클라이언트 시크릿.
  3. APP_KEY: 재생성하면 세션이 깨지고 암호화된 DB 컬럼을 복호화할 수 없게 될 수 있다 — 먼저 영향 범위를 점검하라.
  4. 메일/SMTP, DB 자격증명.

교체의 함정(현장에서 실제로 겪음)

일부 OAuth 제공자는 클라이언트 시크릿 재발급이 기존 refresh_token도 무효화한다 — 저장된 토큰으로의 갱신이 실패해 프로덕션 장애를 일으킨다. 재발급 전에 항상 재인가 흐름을 준비하라. 그리고 로컬 .env / 설정도 갱신하라. 안 그러면 다음 배포가 옛 값을 다시 들여온다.

3단계 — 구조 재배치: 본체를 도큐먼트 루트 바깥으로(진짜 수정)

각 앱을 이렇게 다시 배치하라:

# 1. 본체를 웹 루트 바깥으로 이동(같은 파일시스템 → mv는 즉시, 원자적)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. 웹 루트에는 최소한의 부트스트랩만
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Laravel public/.htaccess 복사, 정적 자산 symlink, config 캐시 클리어

이 형태에서는 .env, .git, vendor/가 애초에 웹 루트에 존재하지 않으니, .htaccess 규칙에 의존하지 않고 안전하다. 전체 절차는 공유 호스팅에서 .env를 공개 웹에서 떼어놓기에 있다.

✗ 위험: 본체가 웹 루트 안

— 웹 경계(HTTP로 읽힘) —

.env · .git · vendor · public

↑ 모두 선 안 = 완전 노출

✓ 안전: 본체가 웹 루트 밖

— 웹 경계(HTTP로 읽힘) —

public/ 만(symlink)

.env · .git은 밖 = 닿지 않음

한 줄이 가른다 — 웹 루트 안이냐 밖이냐. .env와 .git이 그 안에 있으면 누구나 HTTP로 읽을 수 있다.

"교과서에 없는" 함정

  • 서브도메인 범위 문제: 등록된 서브도메인의 도큐먼트 루트를 symlink로 다른 트리에 재지정하면 따라가지 못하고 500을 반환할 수 있다. → 도큐먼트 루트를 실제 디렉터리로 유지하고, 그 안의 작은 index.php에서 절대 경로로 require하라(안정적인 "부트스트랩 리다이렉트" 형태).
  • opcache가 깨진 상태를 기억함: opcache는 실패한 index.php 로드를 기억해, 재구축 후에도 계속 500을 반환할 수 있다. 최후 수단: 파일명을 바꿔라(entry.php 등).
  • '미등록' 서브도메인 도큐먼트 루트가 부모를 통해 읽힘: https://sub.example/는 무효인데 https://example/sub/는 여전히 내용을 보여줄 수 있다 — 놓치기 쉬운 입구. 점검할 때 부모 도메인 경로도 차단하라.
  • config/*.php의 하드코딩 시크릿: 공개 .git과 결합하면 히스토리가 모든 교체 이력을 드러낸다. 항상 env()를 거치고 폴백 기본값에 실제 값을 넣지 마라.

가장 큰 교훈: 이것은 "한 사람의 실수"가 아니다

초기 공유 호스팅 문화는 모든 것을 public_html 바로 아래 두었다. (public/만 노출하도록 기대하는) Laravel이 거기로 흘러들었고, 위험한 "프로젝트를 public_html/<app>/ 아래 두고 그 public/을 노출" 배치가 사실상의 표준이 됐다 — 벤더 자체 문서도 public_html 기반 예시를 쓰는데, 그게 상황을 악화시킨다. 그러니 경계심이 아니라 프로세스로 고쳐라:

프로세스로 재발 방지

  • 배포 기본을 "본체는 도큐먼트 루트 바깥, public/만 symlink"로 만들라.
  • CI/lint 점검: .env를 절대 커밋 안 함; 프로젝트를 public_html/<app> 바로 아래 두지 않음.
  • 배포 후 자가 점검: /.env/.git/config가 외부에서 가져와지지 않는지 확인.

본 사이트는 이 "내 사이트를 스스로 점검하라"는 습관을 학습 트랙에 녹여둔다. → .env와 API 키의 진짜 위험

다음으로 읽기

FAQ

Q.env가 노출됐는데 — 먼저 무엇을 하나요?
A

출혈을 멈추세요(.htaccess로 경로 차단). 그런 다음 .env의 외부 API 키와 OAuth 시크릿을 우선순위대로 교체하고(봤다고 가정), 앱 본체를 웹 루트 바깥으로 옮겨 규칙에 의존하지 않고도 안전하게 만드세요.

QLaravel을 public_html 아래 두는 게 왜 위험한가요?
A

Laravel은 public/만 노출하도록 만들어졌습니다. 프로젝트 전체를 도큐먼트 루트에 두면 그 위의 모든 것 — .env(모든 시크릿), .git(복원 가능한 히스토리), vendor/ — 이 HTTP로 읽힙니다.

Q.htaccess 블랙리스트로 차단하면 충분한가요?
A

응급 처치로는 통하지만 근본적으로 두더지잡기입니다 — 새 파일명마다 구멍이 늘어납니다. 진짜 수정은 구조 변경입니다: 앱 본체를 도큐먼트 루트 바깥에 두고 symlink로 public/만 노출하세요.