웹 개발자를 위한 보안 모범 사례
웹 보안은 선택이 아닙니다. 개발자는 애플리케이션의 모든 계층에 보안을 내재화해야 합니다. 아래에서 오늘부터 적용할 모범 사례를 정리합니다.
비밀번호 보안
평문 비밀번호는 절대 저장하지 않기
비밀번호는 bcrypt, Argon2, scrypt 같은 최신 알고리즘으로 해싱하세요. 이들은 의도적으로 느리게 설계되어 무차별 대입 공격을 현실적으로 불가능한 수준으로 만듭니다.
// 좋은 예: bcrypt 사용
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);
해싱 알고리즘 비교
해싱 알고리즘은 모두 같지 않습니다. 어떤 걸 고를지는 위협 모델과 사용 사례에 따라 달라집니다.
| 알고리즘 | 출력 크기 | 속도 | 사용 사례 | 보안 상태 |
|---|---|---|---|---|
| MD5 | 128비트 | 매우 빠름 | 체크섬, 비보안 해시 | 보안 용도로는 깨짐 |
| SHA-256 | 256비트 | 빠름 | 데이터 무결성, 디지털 서명 | 안전 |
| bcrypt | 184비트 | 느림 (조절 가능) | 비밀번호 해싱 | 안전 |
| Argon2 | 설정 가능 | 느림 (조절 가능) | 비밀번호 해싱 (최신) | 신규 프로젝트에 권장 |
bcrypt와 Argon2는 의도적으로 느립니다. 결함이 아니라 설계입니다. 해시 연산 한 번에 수십, 수백 밀리초가 걸리므로 대규모 무차별 대입이 경제적으로 불가능해집니다.
비밀번호 엔트로피 이해하기
비밀번호 강도는 엔트로피로 수학적으로 측정할 수 있습니다. entropy = log2(charset_size^length). 8자리 소문자(26자) 비밀번호는 약 37.6비트, 대소문자·숫자·기호(95자)가 섞인 16자리 비밀번호는 약 105비트로 기하급수적으로 크래킹이 어려워집니다. 복잡성보다 길이가 더 중요한 이유입니다. 수학적 배경은 비밀번호 엔트로피 가이드를 참고하세요.
비밀번호 관리자 사용 권장
사용자에게 비밀번호 관리자 도입을 권하세요. 사람이 직접 고른 비밀번호는 예측 가능한 패턴을 따르고, 공격자는 이를 사전 공격으로 악용합니다. 비밀번호 관리자는 진짜 무작위 문자열을 생성하고, 여러 서비스 간 비밀번호 재사용을 없애 줍니다. 재사용은 자격 증명 스터핑 공격의 가장 흔한 경로입니다.
충분한 솔트 라운드 사용
솔트 라운드는 연산 비용을 결정합니다. 값이 클수록 더 안전하지만 느려집니다. 대부분의 애플리케이션에는 10~12 라운드가 적절한 균형입니다.
입력 유효성 검사
클라이언트와 서버 양쪽에서 검증
클라이언트 측 유효성 검사는 UX를 개선하지만 보안에는 서버 측 검사가 필수입니다. 클라이언트 입력을 신뢰하지 마세요.
모든 사용자 입력 정제
입력을 정제해 인젝션 공격을 막으세요.
- SQL에는 매개변수화 쿼리 사용
- XSS를 막기 위해 HTML 출력을 이스케이프
- 파일 업로드는 엄격하게 유효성 검사
실제 공격 예시
실제 공격을 이해하면 방어가 한결 쉬워집니다. 사용자 입력을 그대로 HTML에 렌더링하는 댓글 폼을 생각해 봅시다. 공격자가 다음과 같이 입력합니다.
<script>alert('xss')</script>
애플리케이션이 이 입력을 이스케이프 없이 렌더링하면 스크립트는 모든 방문자의 브라우저에서 실행됩니다. 쿠키 탈취, 사용자 리다이렉트, 키로거 주입 등이 가능해집니다. 해결 방법은 항상 출력 문맥에 맞춰 인코딩하는 것이고, HTML 정제에는 DOMPurify 같은 라이브러리를 쓰세요.
SQL 인젝션도 똑같이 위험합니다. 로그인 폼에서 공격자가 사용자명으로 다음을 입력한다고 합시다.
' OR 1=1 --
쿼리가 문자열 연결로 만들어졌다면("SELECT * FROM users WHERE username='" + input + "'") 인증이 완전히 우회됩니다. --가 쿼리의 나머지를 주석 처리하기 때문입니다. 해결책은 항상 매개변수화 쿼리(프리페어드 스테이트먼트)를 쓰는 것이며, 모든 주요 DB 라이브러리가 이를 지원합니다.
// 잘못된 예: 문자열 연결
db.query(`SELECT * FROM users WHERE username='${input}'`);
// 올바른 예: 매개변수화 쿼리
db.query('SELECT * FROM users WHERE username = $1', [input]);
Content Security Policy (CSP)
심층 방어로 Content Security Policy 헤더를 배포하세요. CSP는 어떤 콘텐츠 출처를 신뢰할지 브라우저에 알려 주며, 인라인 스크립트와 허가되지 않은 리소스 로딩을 차단합니다. 코드에 XSS 취약점이 남아 있어도 엄격한 CSP가 주입된 스크립트 실행을 막을 수 있습니다. Content-Security-Policy: default-src 'self'로 시작해 필요에 따라 예외를 추가하세요.
해시 함수
용도에 맞는 해시 선택
용도에 따라 적합한 해시 함수가 다릅니다.
| 사용 사례 | 권장 |
|---|---|
| 비밀번호 | bcrypt, Argon2 |
| 무결성 | SHA-256 |
| 체크섬 | SHA-256, MD5 (비보안) |
| 빠른 해싱 | BLAKE3 |
해시 출력과 충돌 이해하기
MD5는 128비트(16진수 32자), SHA-256은 256비트(16진수 64자) 해시를 생성합니다. 이 차이는 중요합니다. 출력 공간이 클수록 가능한 해시 값이 기하급수적으로 많아져 충돌 가능성이 훨씬 낮아집니다. 충돌이란 서로 다른 두 입력이 같은 해시를 만드는 현상이며, 충돌을 만들 수 있는 공격자는 디지털 서명을 위조하거나 검증된 데이터를 변조할 수 있습니다.
MD5 충돌은 최신 하드웨어에서 몇 초면 만들 수 있습니다. SHA-256은 알려진 실현 가능한 공격이 없어 충돌 저항성을 유지합니다. 맥락에 맞는 알고리즘을 고르는 이유입니다.
- 체크섬과 중복 제거: 보안이 문제가 되지 않을 때는 MD5도 허용됩니다
- 데이터 무결성과 서명: SHA-256이 강력한 충돌 저항성을 갖추고 있습니다
- 비밀번호 저장: bcrypt 또는 Argon2 — 솔트와 의도적 지연이 더해집니다
메시지 인증을 위한 HMAC
메시지의 무결성과 진위성을 함께 검증해야 한다면 HMAC(Hash-based Message Authentication Code)을 쓰세요. HMAC은 해시 함수와 비밀 키를 결합해 키를 아는 당사자만 태그를 생성·검증할 수 있게 합니다. API 인증, 웹훅 검증, 안전한 토큰 생성에 쓰입니다.
보안 용도로 MD5나 SHA-1을 절대 사용하지 않기
MD5와 SHA-1은 보안 용도로는 깨졌습니다. 암호학적 해싱에는 SHA-256이나 SHA-3을 쓰세요.
HTTPS를 모든 곳에
TLS가 실제로 하는 일
TLS(Transport Layer Security)는 세 가지 보호를 담당합니다. 전송 중 암호화(도청 방지), 서버 인증(사칭자가 아닌 진짜 서버와 통신함을 증명), 데이터 무결성(전송 중 변조 탐지). TLS가 없으면 비밀번호, 토큰, 개인 정보 같은 모든 데이터가 평문으로 전송됩니다.
항상 TLS 사용
- 신뢰할 수 있는 CA에서 인증서를 발급 (Let’s Encrypt는 무료이며 완전 자동화됨)
- HTTP를 HTTPS로 리다이렉트
- HSTS 헤더 사용
- TLS 버전을 최신으로 유지
HSTS와 혼합 콘텐츠
HTTP Strict Transport Security(HSTS) 헤더는 사용자가 http://를 입력해도 브라우저가 HTTPS로만 연결하도록 지시합니다. Strict-Transport-Security: max-age=31536000; includeSubDomains를 설정하면 모든 하위 도메인에서 1년 동안 이 정책이 강제됩니다. 공격자가 연결을 HTTP로 다운그레이드하는 SSL 스트리핑 공격을 막아 줍니다.
혼합 콘텐츠에도 유의하세요. HTTPS 페이지가 이미지, 스크립트, 스타일시트를 HTTP로 로드하면 브라우저가 차단하거나 경고합니다. 페이지에 하드코딩된 http:// URL이 있는지 점검하고 모든 리소스에 HTTPS를 강제하세요.
인증
속도 제한 구현
속도 제한으로 무차별 대입 공격을 방어합니다.
- IP당 로그인 시도 횟수 제한
- 실패 후 지연 시간 추가
- 의심스러운 활동에는 CAPTCHA 사용
JWT 인증 기본
JSON Web Token(JWT)은 header.payload.signature 구조의 무상태 인증 메커니즘입니다. 서버가 비밀 키로 서명하고 클라이언트는 이후 요청에 토큰을 포함합니다. 사용자 클레임이 토큰에 담겨 있어 서버가 요청마다 세션 상태를 조회할 필요가 없으므로 분산 시스템과 마이크로서비스에 잘 맞습니다.
액세스 토큰은 짧은 만료 시간(예: 15분)을 설정하고, 새 토큰은 리프레시 토큰으로 받으세요. 리프레시 토큰은 안전하게 저장(localStorage가 아닌 httpOnly 쿠키)하고, 각 토큰이 한 번만 쓰이도록 토큰 로테이션을 구현하세요.
다중 인증 (MFA)
실서비스 수준의 애플리케이션에서 MFA는 선택이 아닙니다. 두 번째 요소(TOTP 코드, 하드웨어 키, 푸시 알림)를 요구하면 비밀번호 유출의 피해를 크게 줄일 수 있습니다. 공격자가 유효한 자격 증명을 얻어도 두 번째 요소 없이는 인증할 수 없습니다.
세션 고정 공격 방지
세션 고정 공격은 공격자가 사용자 인증 전에 알려진 세션 ID를 미리 설정할 때 발생합니다. 로그인 후 공격자는 같은 세션 ID로 인증된 세션을 탈취합니다. 이를 막으려면 인증 성공 후 항상 세션 ID를 재발급하고 이전 세션 ID를 무효화하세요.
안전한 세션 관리
- 암호학적으로 무작위한 세션 ID 생성
- 쿠키에 secure와 httpOnly 플래그 설정
- 세션 타임아웃 구현
- 로그아웃 시 세션 무효화
보안 헤더 체크리스트
올바른 HTTP 응답 헤더 배포는 애플리케이션을 강화하는 가장 손쉬운 방법 중 하나입니다. 필수 보안 헤더 참고표입니다.
| 헤더 | 목적 | 예시 값 |
|---|---|---|
| Content-Security-Policy | XSS와 데이터 인젝션 방어 | default-src 'self' |
| Strict-Transport-Security | HTTPS 연결 강제 | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | MIME 타입 스니핑 방지 | nosniff |
| X-Frame-Options | 클릭재킹 방지 | DENY |
| Referrer-Policy | 리퍼러 정보 제어 | strict-origin-when-cross-origin |
이 헤더들은 웹 서버 레벨(Nginx, Apache), CDN/엣지 레벨(Cloudflare, Vercel), 애플리케이션 프레임워크 어디서나 설정할 수 있습니다. securityheaders.com 같은 도구로 점검하고 A+ 등급을 목표로 하세요. 대부분 한 줄 설정이고 배포 비용이 거의 없습니다.
보안 도구 활용하기
개발에 쓸 만한 보안 도구입니다.
- MD5 해시 생성기 - 체크섬과 레거시 시스템 지원
- UUID 생성기 - 안전한 무작위 식별자 생성
- 랜덤 비밀번호 생성기 - 강력한 비밀번호 생성
인코딩, 해싱, 변환 도구가 개발 워크플로에 어떻게 녹아드는지는 개발자 필수 도구 가이드에서 다룹니다.
자주 묻는 질문
가장 흔한 웹 보안 취약점은 무엇인가요?
OWASP 기준으로 크로스사이트 스크립팅(XSS)이 여전히 가장 흔한 웹 취약점입니다. 적절한 유효성 검사 없이 신뢰할 수 없는 데이터를 웹 페이지에 포함할 때 발생합니다. 모든 사용자 입력을 정제하고, Content Security Policy 헤더를 쓰고, 문맥(HTML, JavaScript, URL, CSS)에 맞춰 출력을 인코딩해 XSS를 방어하세요.
MD5는 비밀번호 해싱에 여전히 안전한가요?
아닙니다. MD5는 비밀번호 해싱에 쓰면 안 됩니다. 연산이 매우 빨라 무차별 대입과 레인보우 테이블 공격에 취약하고, 최신 GPU는 초당 수십억 번의 MD5 해시를 계산합니다. 대신 bcrypt, scrypt, Argon2를 쓰세요. 의도적으로 느리고 내장 솔팅으로 공격에 저항합니다.
2026년 기준 안전한 비밀번호는 얼마나 길어야 하나요?
최소 12자를 권장하지만 16자 이상이면 훨씬 강합니다. 복잡성보다 길이가 중요해서 correct-horse-battery-staple 같은 20자 패스프레이즈가 P@ss1! 같은 짧고 복잡한 비밀번호보다 강합니다. 중요한 계정에는 길이와 무관하게 MFA를 켜세요.
암호화와 해싱의 차이는 무엇인가요?
암호화는 되돌릴 수 있습니다. 키로 원본 데이터를 복호화할 수 있습니다. 해싱은 일방향이며 해시에서 원본을 복원할 수 없습니다. 다시 꺼내 써야 하는 데이터(예: 저장된 사용자 데이터)에는 암호화를, 검증만 하면 되는 데이터(예: 비밀번호, 체크섬)에는 해싱을 쓰세요.
인증 시스템을 직접 구현해야 하나요?
아닙니다. 인증을 처음부터 직접 구축하는 건 위험하고 실수가 잦습니다. Auth0, Firebase Auth, Supabase Auth 같은 검증된 프레임워크나 서비스를 쓰세요. 비밀번호 해싱, 세션 관리, 토큰 로테이션, MFA, 무차별 대입 방어를 모두 처리합니다. 개발 시간은 애플리케이션 고유 기능에 쓰세요.
마무리
보안은 일회성 작업이 아니라 지속적인 과정입니다. 최신 취약점을 파악하고, 코드를 정기적으로 감사하고, 최소 권한 원칙을 따르세요. 사용자는 자신의 데이터를 맡기고 있으니 견고한 실천으로 그 신뢰에 보답하세요.