Security Guide
OWASP Top 10 (2021)
1. Broken Access Control (접근 제어 취약점)
# Bad: 권한 검증 없음
function getUser(userId):
return db.users.findById(userId)
# Good: 권한 검증
function getUser(userId, requestingUser):
if requestingUser.id != userId and not requestingUser.isAdmin:
throw ForbiddenError("Access denied")
return db.users.findById(userId)
2. Cryptographic Failures (암호화 실패)
# Bad: 평문 저장
user.password = inputPassword
# Good: 해시 저장
user.passwordHash = bcrypt.hash(inputPassword, saltRounds=12)
# Bad: 약한 암호화
encrypted = base64.encode(sensitiveData)
# Good: 강력한 암호화
encrypted = AES256.encrypt(sensitiveData, secretKey)
3. Injection (인젝션)
# Bad: SQL Injection 취약
query = "SELECT * FROM users WHERE id = " + userId
# Good: Parameterized Query
query = "SELECT * FROM users WHERE id = ?"
db.execute(query, [userId])
# Bad: XSS 취약
html = "<div>" + userInput + "</div>"
# Good: 이스케이프 처리
html = "<div>" + escapeHtml(userInput) + "</div>"
4. Insecure Design (불안전한 설계)
# Bad: 비밀번호 힌트
function recoverPassword(email, hint):
if user.passwordHint == hint:
return user.password # 절대 금지!
# Good: 토큰 기반 복구
function recoverPassword(email):
token = generateSecureToken()
sendRecoveryEmail(email, token)
storeTokenWithExpiry(token, 1hour)
5. Security Misconfiguration (보안 설정 오류)
# Bad: 디버그 모드 프로덕션 배포
DEBUG = true
SHOW_ERRORS = true
# Good: 환경별 설정
DEBUG = environment == "development"
SHOW_ERRORS = false # 프로덕션에서는 로깅만
# Bad: 기본 비밀번호
ADMIN_PASSWORD = "admin123"
# Good: 환경 변수
ADMIN_PASSWORD = env.get("ADMIN_PASSWORD")
인증 (Authentication)
JWT 보안
# Bad: 약한 시크릿
secret = "mysecret"
# Good: 강력한 시크릿
secret = env.get("JWT_SECRET") # 최소 256비트
# Bad: 토큰에 민감정보
payload = {userId, email, password, ssn}
# Good: 최소 정보만
payload = {userId, role, exp}
# 토큰 검증
function verifyToken(token):
try:
decoded = jwt.verify(token, secret, algorithms=["HS256"])
if decoded.exp < now():
throw TokenExpiredError()
return decoded
catch:
throw InvalidTokenError()
세션 관리
# 세션 설정 권장사항
session.config = {
httpOnly: true, # JavaScript 접근 차단
secure: true, # HTTPS만 허용
sameSite: "strict", # CSRF 방지
maxAge: 3600, # 1시간 만료
}
# 로그아웃 시 세션 무효화
function logout(sessionId):
session.destroy(sessionId)
blacklist.add(sessionId)
인가 (Authorization)
RBAC (Role-Based Access Control)
# 역할 정의
ROLES = {
admin: ["read", "write", "delete", "admin"],
editor: ["read", "write"],
viewer: ["read"],
}
# 권한 검증 미들웨어
function requirePermission(permission):
return function(request, next):
userRole = request.user.role
if permission not in ROLES[userRole]:
throw ForbiddenError()
next()
# 사용
@requirePermission("write")
function updatePost(postId, content):
...
리소스 소유권 검증
# Bad: ID만 검증
function deletePost(postId):
db.posts.delete(postId)
# Good: 소유권 검증
function deletePost(postId, userId):
post = db.posts.findById(postId)
if post.authorId != userId and not user.isAdmin:
throw ForbiddenError("Not authorized")
db.posts.delete(postId)
입력 검증
화이트리스트 검증
# Bad: 블랙리스트
if "<script>" not in input:
process(input)
# Good: 화이트리스트
ALLOWED_TAGS = ["p", "b", "i", "a"]
sanitized = sanitizeHtml(input, allowedTags=ALLOWED_TAGS)
# 타입 검증
function validateUser(data):
schema = {
email: {type: "email", required: true},
age: {type: "integer", min: 0, max: 150},
name: {type: "string", maxLength: 100},
}
return validate(data, schema)
파일 업로드
# Bad: 확장자만 검증
if filename.endswith(".jpg"):
save(file)
# Good: MIME 타입 + 확장자 + 크기 검증
ALLOWED_TYPES = ["image/jpeg", "image/png"]
MAX_SIZE = 5 * 1024 * 1024 # 5MB
function validateUpload(file):
if file.mimetype not in ALLOWED_TYPES:
throw InvalidFileError()
if file.size > MAX_SIZE:
throw FileTooLargeError()
# 파일 내용 검증 (매직 넘버)
if not isValidImage(file.buffer):
throw InvalidFileError()
보안 체크리스트
인증
인가
데이터 보호
입력 검증
관련 스킬
api-design: API 보안 설계
code-quality: 안전한 에러 처리