설명
디렉터리 리스팅(Directory Listing)은 웹 서버가 특정 폴더를 요청받았을 때, 그 폴더 안에 있는 파일 목록을 그대로 보여주는 기능입니다. Go의 http.FileServer는 디렉터리에 index.html이 없으면 목록을 제공할 수 있습니다. 별도의 접근 통제 없이 외부에 제공하면 디렉터리 구조와 파일 이름이 노출될 수 있습니다.
공격자는 이 기능을 악용해 config 파일, backup 파일, 임시 로그, 숨겨 두었다고 생각한 스크립트 등 의도치 않은 민감 파일을 찾아내고, 이를 이용해 추가 공격(계정 탈취, 코드 분석, 취약점 탐색 등)을 수행할 수 있습니다.
잠재적 영향
- 민감 정보 노출: 디렉터리 구조, 파일 이름, 백업/설정 파일 등의 정보가 외부에 그대로 노출될 수 있습니다.
- 의도하지 않은 파일 발견: 링크를 숨기는 것은 접근 통제가 아닙니다. 목록에서 발견한 파일에 읽기 권한이 있으면 그 내용에도 접근할 수 있습니다.
- 추가 공격 발판 제공: 노출된 소스 코드, 설정 파일 등을 분석해 SQL Injection, 인증 우회 등 다른 취약점을 찾는 데 활용될 수 있습니다.
해결 방법
http.FileServer에 프로젝트 루트나 필요 이상으로 넓은 디렉터리를 연결하지 마세요.- 공개할 파일만 담은 전용 디렉터리를 제한된 경로에 연결하고, 민감 파일과 디렉터리 밖을 가리키는 심볼릭 링크가 없는지 확인하세요. 신뢰할 수 없는 사용자의 파일·링크 생성을 막으세요.
- 정적 파일을 별도 서버나 프록시에서 제공하더라도 불필요한 디렉터리 목록을 비활성화하세요.
- 보호된 파일은 사용자 권한을 확인한 뒤
ServeContent,ServeFile등으로 제공하세요. 이 함수들이 인가를 대신하지는 않습니다. - 배포 전 정적 디렉터리의 설정,
.env,.git, 백업, 로그 파일을 점검하세요.
예시
변경 후 예시는 목록 제공을 막는 데 초점을 둡니다. ./public에는 공개용 파일만 두고 외부를 가리키는 심볼릭 링크를 금지해야 합니다. 일부 이름·확장자 검사만으로 모든 민감 파일을 식별할 수는 없습니다.
변경 전
go
package main
import (
"log"
"net/http"
)
// 취약한 예: 프로젝트 루트 디렉터리를 그대로 노출
func main() {
// 현재 디렉터리(".") 전체를 FileServer로 노출
fs := http.FileServer(http.Dir("."))
// 서버에서 http://localhost:8080/ 에 접속하면
// 디렉터리 목록과 그 안의 모든 파일이 보일 수 있음
http.Handle("/", fs)
log.Fatal(http.ListenAndServe(":8080", nil))
}
변경 후
go
package main
import (
"log"
"net/http"
"os"
"path/filepath"
)
// 안전한 예:
// 1) 정적 파일 전용 디렉터리만 노출
// 2) 민감 파일은 사전에 필터링
// 정적 파일만 두는 디렉터리
const staticDir = "./public"
// 민감 확장자/파일 이름을 간단히 필터링하는 예시
func isForbidden(path string) bool {
base := filepath.Base(path)
if base == ".env" || base == "config.yaml" || base == "config.yml" {
return true
}
ext := filepath.Ext(path)
switch ext {
case ".log", ".bak", ".old", ".zip":
return true
}
return false
}
func secureStaticHandler(w http.ResponseWriter, r *http.Request) {
// 정적 파일 경로 계산
cleanPath := filepath.Clean(r.URL.Path)
fullPath := filepath.Join(staticDir, cleanPath)
if isForbidden(fullPath) {
http.NotFound(w, r)
return
}
// 파일 존재 여부 체크
info, err := os.Stat(fullPath)
if err != nil || info.IsDir() {
// 디렉터리 자체를 노출하지 않음 (목록 제공 X)
http.NotFound(w, r)
return
}
http.ServeFile(w, r, fullPath)
}
func main() {
// /static 경로로만 정적 파일 제공
http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
// /static 접두어 제거 후 내부 핸들러로 위임
r.URL.Path = r.URL.Path[len("/static"):]
secureStaticHandler(w, r)
})
// 그 외 경로는 일반 핸들러 사용
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<h1>Home</h1>"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
설명:
- 변경 전: 현재 디렉터리를 인증 없이 제공합니다. 인덱스 파일이 없으면 목록이 보일 수 있고, 읽을 수 있는 설정·로그·백업 등의 내용도 노출될 수 있습니다.
- 변경 후:
/static/경로에서 전용 디렉터리의 파일을 제공하고, 디렉터리 요청에는 404를 반환합니다.isForbidden은 일부 파일만 거부하므로 공개 디렉터리의 배포 내용과 쓰기 권한도 통제해야 합니다.