2026년 API 보안: 생산 시스템을 파괴하는 공격들

발행: (2026년 5월 24일 AM 05:20 GMT+9)
9 분 소요
원문: Dev.to

API 보안 2026: 실제 운영 시스템을 파괴하는 공격들

매주 새로운 기업이 데이터 유출을 발표합니다. 공격자는 제로데이 혹은 정교한 악성코드를 사용하지 않습니다. 수년간 존재해 온 동일한 API 취약점을 악용하고 있습니다. 2026년 현재, 대부분의 팀에게 API 보안은 여전히 사후 고려 사항이며, 공격자는 이를 잘 알고 있습니다.

지난 6개월 동안 실제 API 침해 사례를 분석했습니다. 실제로 운영 시스템을 위협하고 있는 내용은 다음과 같습니다.

OWASP API 보안 Top 10 (2019와 거의 동일)

  • 객체 수준 권한 부여 오류 (BOLA)
  • 인증 오류
  • 객체 속성 수준 권한 부여 오류
  • 무제한 자원 소비
  • 기능 수준 권한 부여 오류
  • 대량 할당 (Mass Assignment)
  • 보안 구성 오류
  • 인젝션
  • 부적절한 인벤토리 관리
  • API의 안전하지 않은 사용

이것들은 이론이 아닙니다. 지난 18개월 동안 주요 침해 사건마다 각각이 실제로 악용되었습니다.

객체 수준 권한 부여 오류 (BOLA)

BOLA는 다른 어떤 취약점보다 더 많은 데이터 유출을 일으킵니다. 패턴은 언제나 같습니다: API 엔드포인트가 객체 ID를 노출하고, API가 인증된 사용자가 실제로 그 객체를 소유하고 있는지 확인하지 않습니다.

// 취약한 API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
  const order = await Order.findById(req.params.orderId);
  res.json(order); // 소유권 검사 없음!
});

// 공격: 주문 ID를 순차적으로 조회
// curl https://api.example.com/api/orders/1
// curl https://api.example.com/api/orders/2
// curl https://api.example.com/api/orders/3
// ... 50,000개의 고객 레코드 추출
// 안전한 API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
  // 명시적인 소유권 검사
  const order = await Order.findOne({
    _id: req.params.orderId,
    userId: req.user.id  // 항상 소유자 기준 필터링
  });

  if (!order) {
    return res.status(404).json({ 
      error: '주문을 찾을 수 없습니다' 
    });
  }

  res.json(order);
});

핵심 교훈: 인증만 확인하지 말고 반드시 소유권을 검증하세요. 인증된다고 해서 해당 객체에 대한 권한이 있는 것은 아닙니다.

JSON Web Token (JWT) 오용

JWT가 어디에나 사용되지만 구현이 잘못된 경우가 많습니다. 실제 프로덕션 API에서 발견한 JWT 취약점 몇 가지를 소개합니다.

// 취약: 서버가 모든 알고리즘을 허용
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['HS256', 'RS256'] // 이렇게 하면 안 됩니다
});

// 공격: RS256을 HS256으로 바꾸고 공개키로 서명
// 서버가 대칭키와 비대칭키를 동일하게 사용하므로
// 공격자는 RSA 공개키로 HS256 토큰을 위조할 수 있습니다

// 안전: 알고리즘 화이트리스트 적용
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'] // 의도한 알고리즘만 허용
});
// 취약: 일부 라이브러리가 'none' 알고리즘을 허용
jwt.verify(token, '', { algorithms: ['none'] }); 
// 결과: {"alg":"none","typ":"JWT"}
// 토큰 예시: eyJhbGciOiJub25lIiwidHlwIjoiand0In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

// 안전: 'none'을 명시적으로 거부
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],
  alloweds: { algorithms: ['HS256', 'RS256'] } // none은 거부
});

// 또는 기본적으로 'none'을 거부하는 라이브러리 사용
const decoded = jwt.verify(token, publicKey);

무제한 자원 소비

이 취약점은 과소평가되지만 점점 더 흔해지고 있습니다. 서비스가 다운될 필요는 없습니다—운영 비용만 급격히 상승하면 됩니다.

# 취약: 페이지네이션 및 제한 없음
@app.get("/api/search")
def search(q: str):
    results = db.query(f"SELECT * FROM products WHERE name LIKE '%{q}%'")
    return results  # 모든 매치를 반환, 공격자가 결과 집합 크기 제어

# 공격: "a" 검색 → 2.3백만 행 반환
# DB가 전체 테이블 스캔 수행
# 비용: 쿼리당 $0.02 × 10,000쿼리/분 = $200/분
# 안전: 엄격한 페이지네이션 및 쿼리 제한
@app.get("/api/search")
def search(
    q: str, 
    limit: int = Query(default=20, ge=1, le=100),   # 최대 100
    offset: int = Query(default=0, ge=0, le=10000)   # 최대 오프셋
):
    # 파라미터화된 쿼리로 인젝션 방지
    results = db.query(
        "SELECT * FROM products WHERE name LIKE %s LIMIT %s OFFSET %s",
        (f"%{q}%", limit, offset)
    )

    # 쿼리 복잡도 제한 추가
    if len(q) > 100:
        raise HTTPException(400, "쿼리가 너무 깁니다")

    return results
# 취약: 이메일 전송에 레이트 제한 없음
@app.post("/api/password-reset")
def request_reset(email: str):
    user = db.find_user(email)
    if user:
        send_email(user.email, generate_token())  # 제한 없음
    return {"message": "해당 이메일이 존재하면 비밀번호 재설정 메일을 보냈습니다"}

# 공격: 초당 1,000개의 이메일을 SMTP 제공업체에 전송
# 비용: 이메일당 $0.10 × 86,400,000 이메일/일 = $8.6M/일
# 안전: IP와 이메일당 엄격한 레이트 제한
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/password-reset")
@limiter.limit("3/hour")  # IP당 시간당 3회 시도 제한
def request_reset(email: str, request: Request):
    user = db.find_user(email)
    if user:
        send_email(user.email, generate_token())
    # 열거 방지를 위해 항상 동일한 메시지 반환
    return {"message": "해당 이메일이 존재하면 비밀번호 재설정 메일을 보냈습니다"}

대량 할당 (Mass Assignment)

// 취약: 클라이언트 입력을 무조건 신뢰
app.post('/api/profile', authMiddleware, async (req, res) => {
  await User.updateOne(
    { _id: req.user.id },
    { $set: req.body }  // 클라이언트가 모든 필드를 설정 가능
  );
});

// 공격: 다음과 같은 요청 전송
// POST /api/profile
// {"name":"John","role":"admin","isVerified":true,"creditLimit":1000000}

// 결과: 일반 사용자가 관리자 권한과 무제한 신용 한도를 얻게 됨
// 안전: 허용 필드 화이트리스트 적용
app.post('/api/profile', authMiddleware, async (req, res) => {
  const allowedFields = ['name', 'bio', 'avatarUrl', 'timezone'];
  const updates = {};

  for (const field of allowedFields) {
    if (field in req.body) {
      updates[field] = req.body[field];
    }
  }

  await User.updateOne(
    { _id: req.user.id },
    { $set: updates }
  );

  res.json({ success: true });
});

권한 검사 없는 관리자 엔드포인트

# 취약: 역할 검증 없이 관리자 엔드포인트 존재
@app.post("/api/admin/users/delete")
def delete_user(user_id: str):
    db.delete_user(user_id)
    return {"success": True}

# 공격: 이 엔드포인트를 찾은 누구든지 모든 사용자를 삭제 가능
# 인증·인가 검사 없음
# 안전: 명시적인 역할 요구
from functools import wraps

def require_admin(f):
    @wraps(f)
    async def decorated(*args, **kwargs):
        if not request.user or request.user.role != 'admin':
            return {"error": "Forbidden"}, 403
        return await f(*args, **kwargs)
    return decorated

@app.post("/api/admin/users/delete")
@require_admin
@require_auth
def delete_user(user_id: str):
    db.delete_user(user_id)
    return {"success": True}

자동화된 보안 테스트 예시

import httpx
import pytest

class TestAPISecurity:
    """API 보안 테스트 스위트 – 배포 전 스테이징 환경에서 실행"""

    def test_bola_object_level_access(self):
        """사용자가 다른 사용자의 리소스에 접근하지 못하도록 검증"""
        user1_token = self.get_token("user1@example.com")
        user2_resource = self.create_resource("user2@example.com")

        # 사용자 1이 사용자 2의 리소스에 접근 시도
        response = httpx.get(
            f"{BASE_URL}/api/resources/{user2_resource.id}",
            headers={"Authorization": f"Bearer {user1_token}"}
        )

        assert response.status_code == 403, "BOLA 취약점: 사용자가 타인의 리소스에 접근 가능!"

    def test_jwt_algorithm_confusion(self):
        """JWT 알고리즘 혼동 공격 테스트"""
        token = self
0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.