목차
- SaaS 파일 저장의 구조적 문제
- Fiverr 사건 — 무엇이 노출됐나
- Google Dorking으로 직접 확인하는 법
- SaaS 파일 보안 — 뭘 봐야 하는가
- 업로드 전 클라이언트 측 암호화
- 실무 체크리스트 — SaaS에 파일 올리기 전
- SaaS 선택 시 파일 보안 확인 방법
- 언제 신경 쓰고 언제 넘어갈 것인가
SaaS 파일 저장의 구조적 문제
SaaS 플랫폼에서 파일을 주고받을 때, 그 파일이 어디에 어떻게 저장되는지 신경 쓰는 사람은 생각보다 적다. 대부분은 "플랫폼이 알아서 관리하겠지"라고 생각한다. 문제는 그 "알아서"의 실체가 S3 버킷에 퍼블릭 URL을 붙여놓은 것일 수 있다는 점이다.
최근 Hacker News에서 Fiverr의 파일 저장 방식이 화제가 됐다. 고객이 프리랜서에게 전달한 파일 — 소스코드, 디자인 원본, 사업 계획서 같은 것들이 구글 검색에 그대로 노출되고 있었다. site:fiverr-res.cloudinary.com 같은 검색어로 누구나 접근할 수 있었다는 거다.
외주 맡기면서 .env 파일이나 DB 스키마가 담긴 문서를 아무 생각 없이 올리는 경우가 있을 텐데, 이번 사건은 그게 얼마나 위험한 행위인지 보여준 케이스다.
Fiverr 사건 — 무엇이 노출됐나
핵심은 단순하다. Fiverr에서 주문 과정 중 전달되는 파일들이 인증 없이 접근 가능한 CDN URL로 저장됐고, 해당 URL이 robots.txt로 차단되지 않아 구글 크롤러가 수집해간 것이다.
노출된 파일 유형
HN 스레드에서 보고된 내용을 보면, 노출 범위가 꽤 넓었다:
- 로고, 배너 같은 디자인 시안 (이건 그나마 피해가 적다)
- WordPress 사이트의 전체 소스코드를 ZIP으로 압축해서 전달한 케이스
- AWS 크레덴셜이 포함된 설정 파일
- 고객사의 내부 문서, 사업 계획서
디자인 파일 정도야 유출되어도 큰 문제가 아닐 수 있지만, AWS 크레덴셜이 포함된 파일이 공개 URL에 올라가 있었다면 이건 심각한 보안 사고다. 실제로 공격자가 이런 URL 패턴을 자동으로 스캔하는 도구를 돌리고 있다는 건 이미 알려진 사실이다.
기술적 원인
파일 저장 아키텍처를 추정해보면 이렇다:
사용자 업로드 → Fiverr 서버 → Cloudinary/S3 (퍼블릭 버킷)
↓
인증 없는 고정 URL 생성
↓
Google 크롤러 인덱싱
문제가 되는 지점은 두 가지다. 첫째, 파일 URL에 만료 시간이 없다. S3 Presigned URL을 쓰면 일정 시간 후 접근이 차단되는데, Fiverr는 영구적인 퍼블릭 URL을 사용한 것으로 보인다. 둘째, CDN 도메인에 대한 robots.txt 설정이 없거나 부실했다. 크롤러 입장에서는 "인덱싱해도 된다"는 신호를 받은 셈이다.
Google Dorking으로 직접 확인하는 법
내가 이 사건을 보고 제일 먼저 한 건, 우리 팀에서 쓰는 SaaS들의 파일이 혹시 노출되어 있는지 확인하는 거였다. Google Dorking은 특별한 도구 없이 구글 검색만으로 할 수 있어서 진입 장벽이 낮다.
기본 검색 연산자 몇 개만 알면 된다:
# 특정 SaaS 도메인의 파일 인덱싱 확인
site:storage.example-saas.com filetype:pdf
# 특정 파일 확장자로 범위 좁히기
site:cdn.example-saas.com filetype:zip OR filetype:sql OR filetype:env
# 회사명이 포함된 노출 파일 검색
"mycompany" filetype:xlsx site:*.cloudinary.com
실제로 돌려보면 생각보다 많은 결과가 나올 수 있다. 한번은 우리 팀에서 쓰던 프로젝트 관리 도구의 첨부파일이 구글에 인덱싱되어 있는 걸 발견한 적이 있다. 파일 자체는 내부 회의록이라 민감도가 높진 않았지만, URL 패턴을 알면 다른 파일도 유추할 수 있다는 게 찜찜했다.
자동화하려면 Python 스크립트로 만들 수도 있다:
import requests
from urllib.parse import quote
# Google Custom Search API 사용
# (주의: Google 검색을 직접 스크래핑하면 차단당한다)
API_KEY = "your-api-key"
CSE_ID = "your-cse-id"
def check_exposure(domain: str, filetypes: list[str]) -> list[dict]:
"""SaaS 도메인에서 노출된 파일 검색"""
results = []
for ft in filetypes:
query = f"site:{domain} filetype:{ft}"
url = (
f"https://www.googleapis.com/customsearch/v1"
f"?key={API_KEY}&cx={CSE_ID}&q={quote(query)}"
)
resp = requests.get(url, timeout=10)
data = resp.json()
# 검색 결과가 있으면 노출된 것
if data.get("searchInformation", {}).get("totalResults", "0") != "0":
results.append({
"domain": domain,
"filetype": ft,
"count": data["searchInformation"]["totalResults"],
"sample": data.get("items", [{}])[0].get("link", "N/A")
})
return results
# 점검할 SaaS 도메인 목록
domains = [
"storage.slack-files.com",
"files.notion.so",
"cdn.fiverr-res.cloudinary.com",
]
sensitive_types = ["env", "sql", "zip", "csv", "xlsx", "json", "pem"]
for domain in domains:
exposed = check_exposure(domain, sensitive_types)
if exposed:
print(f"[경고] {domain}에서 파일 노출 감지:")
for item in exposed:
print(f" - .{item['filetype']}: {item['count']}건")
이건 주기적으로 돌려야 의미가 있다. cron으로 주 1회 정도 걸어두면 새로운 노출이 생겼을 때 빠르게 잡을 수 있다.
SaaS 파일 보안 — 뭘 봐야 하는가
SaaS를 선택할 때 기능, 가격, UX만 보는 경우가 많은데, 파일 저장 방식도 확인해야 한다. 아래는 점검 기준이다.
| 점검 항목 | 안전 | 위험 |
|---|---|---|
| 파일 URL 인증 | Presigned URL (만료 시간 있음) | 고정 퍼블릭 URL |
| robots.txt | 파일 저장 경로 Disallow | 설정 없음 or Allow |
| 접근 권한 | 로그인한 사용자만 접근 | URL만 알면 누구나 접근 |
| 파일 만료 | 일정 기간 후 자동 삭제 | 영구 보관 |
| 전송 암호화 | TLS + 저장 시 암호화 (AES-256) | TLS만 적용 |
| 감사 로그 | 파일 접근 로그 제공 | 로그 없음 |
대부분의 SaaS가 TLS는 적용하고 있다. 전송 중 암호화는 거의 기본이 된 상태다. 문제는 저장 후의 접근 제어인데, 여기서 차이가 크게 갈린다.
Presigned URL vs 퍼블릭 URL
AWS S3를 예로 들면, 같은 파일이라도 접근 방식이 완전히 다르다:
import boto3
from datetime import datetime
s3_client = boto3.client("s3")
# 나쁜 예: 퍼블릭 URL (영구적, 누구나 접근 가능)
bad_url = "https://my-bucket.s3.amazonaws.com/uploads/client-data.zip"
# 좋은 예: Presigned URL (1시간 후 만료)
good_url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": "my-bucket",
"Key": "uploads/client-data.zip",
},
ExpiresIn=3600, # 3600초 = 1시간
)
# 결과: https://my-bucket.s3.amazonaws.com/uploads/client-data.zip
# ?X-Amz-Expires=3600&X-Amz-Signature=abc123...
Presigned URL은 서명 파라미터가 붙어서 만료 시간이 지나면 AccessDenied를 반환한다. 구글 크롤러가 수집해가더라도 만료된 URL이면 인덱싱이 풀리게 된다.
robots.txt가 왜 중요한가
CDN이나 파일 저장소 도메인에 robots.txt를 설정하는 건 가장 기본적인 방어다. 이건 간단하다.
# cdn.example.com/robots.txt
User-agent: *
Disallow: /uploads/
Disallow: /files/
Disallow: /attachments/
물론 robots.txt는 강제가 아니라 권고 사항이라, 악의적인 크롤러는 무시할 수 있다. 그래서 이것만으로는 부족하고 인증 기반 접근 제어와 함께 써야 한다.
업로드 전 클라이언트 측 암호화
SaaS 플랫폼의 보안을 100% 신뢰할 수 없다면, 업로드 전에 클라이언트 측에서 암호화하는 방법이 있다. 실제로 우리 팀에서는 외부 플랫폼에 민감한 파일을 보낼 때 이 방식을 쓰기 시작했다.
from cryptography.fernet import Fernet
import base64
import hashlib
def encrypt_file(file_path: str, password: str) -> str:
"""파일을 암호화해서 .enc 파일로 저장"""
# 패스워드에서 키 생성
key = base64.urlsafe_b64encode(
hashlib.sha256(password.encode()).digest()
)
fernet = Fernet(key)
with open(file_path, "rb") as f:
original = f.read()
encrypted = fernet.encrypt(original)
output_path = file_path + ".enc"
with open(output_path, "wb") as f:
f.write(encrypted)
print(f"암호화 완료: {output_path} ({len(encrypted)} bytes)")
return output_path
# 사용 예시
# SaaS에 올리기 전에 암호화
encrypt_file("database_schema.sql", "shared-secret-with-freelancer")
이렇게 하면 설령 URL이 노출되더라도 파일 내용은 보호된다. 수신자에게 패스워드를 별도 채널(Signal, 전화 등)로 전달하면 된다.
간단한 방법으로는 그냥 zip에 패스워드를 거는 것도 있다:
# 패스워드 보호된 ZIP 생성
zip -e sensitive_files.zip database_schema.sql config.json
# Enter password: ********
도구가 뭐든 핵심은 "플랫폼에 올라간 파일이 평문이면 안 된다"는 것이다.
실무 체크리스트 — SaaS에 파일 올리기 전
SaaS 보안 정책을 전부 분석하는 건 현실적으로 어렵다. 대신 실무에서 바로 적용할 수 있는 기준을 몇 가지로 줄였다.
파일 민감도 분류
모든 파일을 같은 수준으로 보호할 필요는 없다. 올리기 전에 3초만 생각하면 된다:
- Level 3 (절대 올리면 안 됨):
.env,.pem, 크레덴셜, API 키, SSH 키. 이런 건 SaaS에 올리는 것 자체가 잘못이다. - Level 2 (암호화 후 올릴 것): 소스코드, DB 스키마, 내부 문서, 고객 데이터가 포함된 파일.
- Level 1 (그냥 올려도 됨): 공개 가능한 디자인 시안, 마케팅 자료, 공개된 문서.
Level 3에 해당하는 파일을 Fiverr 같은 플랫폼에 올리는 사람이 있다는 게 놀랍지만, 실제로 HN 스레드에서 AWS 크레덴셜이 노출된 사례가 보고됐다.
Git 레포에서 민감 파일 걸러내기
외주를 줄 때 레포를 통째로 ZIP으로 묶어서 보내는 경우가 있는데, 이때 .env나 시크릿 파일이 같이 딸려가는 경우가 많다. 보내기 전에 한 번 걸러주는 스크립트를 만들어 두면 편하다:
#!/bin/bash
# export-safe.sh — 민감 파일 제외하고 ZIP 생성
EXCLUDE_PATTERNS=(
"*.env"
"*.env.*"
"*.pem"
"*.key"
"*credentials*"
"*secret*"
".git"
"node_modules"
)
EXCLUDE_ARGS=""
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$pattern"
done
# tar로 묶어서 민감 파일 제외
tar czf project-export.tar.gz $EXCLUDE_ARGS ./src ./docs ./README.md
echo "안전한 파일만 묶었다: project-export.tar.gz"
echo "제외된 패턴:"
printf ' - %s\n' "${EXCLUDE_PATTERNS[@]}"
이 정도만 해도 "실수로 .env 보냄" 사고는 막을 수 있다.
SaaS 선택 시 파일 보안 확인 방법
새로운 SaaS를 도입할 때 보안 문서를 확인하는 법을 몰라서 그냥 넘기는 경우가 많다. 확인 포인트는 의외로 간단하다.
첫째, 해당 SaaS의 보안 페이지를 찾는다. 대부분 trust.example.com이나 example.com/security에 있다. SOC 2 인증 여부가 나와 있으면 최소한의 접근 제어는 갖추고 있다고 볼 수 있다.
둘째, 파일 URL 패턴을 직접 확인한다. 테스트 파일 하나를 올리고, 공유 링크를 받아본다. 그 URL을 시크릿 브라우저(로그인 안 된 상태)에서 열어본다. 열리면 인증 없이 접근 가능하다는 뜻이다.
셋째, robots.txt를 확인한다. 파일이 저장된 CDN 도메인의 /robots.txt를 열어보면 크롤링 정책을 알 수 있다:
# CDN 도메인의 robots.txt 확인
curl -s https://cdn.example-saas.com/robots.txt
이 세 가지만 확인해도 해당 SaaS의 파일 보안 수준을 대략 파악할 수 있다. 시간은 5분이면 충분하다.
언제 신경 쓰고 언제 넘어갈 것인가
모든 SaaS 사용에 이 수준의 점검을 적용하는 건 과하다. 기준을 잡아야 한다.
반드시 점검해야 하는 경우: 소스코드, 인프라 설정, 고객 데이터, 계약서 등 유출 시 실질적 피해가 발생하는 파일을 외부 SaaS에 업로드하는 상황. Fiverr, Upwork 같은 프리랜서 플랫폼에 개발 관련 파일을 올릴 때가 대표적이다.
적당히 넘어가도 되는 경우: 이미 공개된 정보이거나, 유출돼도 피해가 없는 파일. 마케팅 배너 시안을 Fiverr에 올리는 건 크게 문제될 게 없다.
실질적인 액션은 세 가지다. 하나, 지금 쓰고 있는 SaaS에 올린 파일 중 Level 2 이상이 있는지 확인한다. 둘, Google Dorking으로 해당 SaaS 도메인의 파일 노출 여부를 검색해본다. 셋, 앞으로 민감 파일을 올릴 때는 클라이언트 측 암호화를 적용한다. 이 세 가지면 Fiverr 같은 사고에서 최소한 내 파일은 지킬 수 있다.
관련 글
- OpenSSL 4.0.0 업그레이드 회고 — 올렸다가 롤백하고 배운 것들 – OpenSSL 4.0.0이 나오자마자 스테이징에 올려봤다. 결과는 롤백이었다. 프론트엔드 출신 백엔드 개발자가 시스템 라이브러리 메이저 업…
- OpenSSL 4.0.0 프로덕션 마이그레이션, 지금 올려도 되는가 – OpenSSL 메이저 업그레이드는 라이브러리 하나가 아니라 디펜던시 체인 전체를 흔드는 작업이다. 4.0.0 프로덕션 적용 타이밍을 3.0…
- 로컬 LLM 보안 운영 — Ollama로 사내 데이터 유출 없이 AI 쓰는 법 – 사내 외부 LLM API가 차단된 뒤 Ollama로 로컬 LLM 보안 운영 환경을 구축했다. 기본 설정의 보안 구멍부터 네트워크 격리, 접…