요금 제한

속도 제한은 API 남용, DoS 공격, 스크래핑으로부터 시스템을 보호하는 기본적인 방어 메커니즘입니다.

알고리즘 비교

알고리즘특성장점단점
Fixed Window정해진 시간 범위 내에서 계산구현이 간단하고 메모리 효율적창 경계에서 버스트 발생
Sliding Window Log레코드 요청 타임스탬프정밀한 제어높은 메모리 소비량
Sliding Window Counter이전 창과 현재 창의 가중 평균좋은 균형다소 복잡함
Token Bucket액세스를 위해 토큰을 소비합니다.제한을 적용하면서 버스트를 허용합니다.매개변수 조정 필요
Leaky Bucket일정한 속도로 요청 처리안정적인 출력 속도버스트 처리에 취약함

Express.js의 속도 제한 구현

JavaScript (Express)멀티 레이어 속도 제한
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// 글로벌 제한: 모든 API에 적용
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 100,
  standardHeaders: true,    // RateLimit-* 헤더 반환
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

// 인증 엔드포인트의 경우: 더 엄격한 제한 사항
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // 로그인 시도는 15분당 5회로 제한됩니다.
  skipSuccessfulRequests: true, // 성공한 요청은 계산되지 않습니다.
});

// 분산 환경의 경우: Redis 백엔드
const distributedLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
  windowMs: 60 * 1000,
  max: 30,
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);

응답 헤더

HTTP Response HeadersRFC 6585 / draft-ietf-httpapi-ratelimit-headers
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1672531200
Retry-After: 30
속도 제한 키

IP 주소 외에 API 키, 사용자 ID 또는 엔드포인트와 같은 복합 키를 사용하여 속도 제한을 적용하면 합법적인 사용자에게 미치는 영향을 최소화할 수 있습니다.

🔍 입력 유효성 검사

모든 API 입력에는 악성 데이터가 포함될 수 있습니다. 항상 서버 측에서 유효성을 검사하세요.

유효성 검사 원칙

허용 목록 접근 방식

허용된 값의 목록을 정의합니다. 거부 목록(차단 목록)보다 안전합니다. 새로운 공격 패턴을 처리할 수 있습니다.

유형, 길이 및 범위

데이터 유형, 최대 문자 수, 숫자 상한/하한을 엄격하게 확인합니다. 버퍼 오버플로우에 대한 근본적인 방어.

살균 대 거부

유효하지 않은 입력을 거부하는 것이 '수정'하는 것보다 안전합니다. 위생 처리로 인해 예기치 않은 변형이 발생할 수 있습니다.

JSON 스키마를 사용한 유효성 검사

JavaScript (Express + Ajv)스키마 유효성 검사
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);

// 사용자 생성 API 스키마
const createUserSchema = {
  type: 'object',
  required: ['name', 'email'],
  additionalProperties: false,
  properties: {
    name: {
      type: 'string',
      minLength: 1,
      maxLength: 100,
      pattern: '^[a-zA-Z0-9\\s\\-]+$', // 허용된 문자로 제한
    },
    email: {
      type: 'string',
      format: 'email',
      maxLength: 254,
    },
    age: {
      type: 'integer',
      minimum: 0,
      maximum: 150,
    },
  },
};

// 유효성 검사 미들웨어
function validateBody(schema) {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors,
      });
    }
    next();
  };
}

app.post('/api/users', validateBody(createUserSchema), createUser);

일반적인 공격 및 검증 대책

공격대응 방안
SQL 주입매개변수화된 쿼리, ORM 사용db.query('SELECT * FROM users WHERE id = ?', [id])
NoSQL 인젝션유형 검사, $ 연산자 위생 처리입력이 문자열인지 확인(객체 거부)
XSS(API 응답을 통한)콘텐츠 유형 지정, 출력 이스케이프Content-Type: application/json
경로 탐색입력에서 경로 구분 기호 제거path.basename()로 정규화하기
XXE(XML 외부 엔티티)외부 엔티티 해상도 비활성화XML 구문 분석기 설정에서 비활성화

🌐 CORS(교차 출처 리소스 공유)

CORS는 브라우저의 동일 출처 정책을 제어하는 메커니즘입니다. 잘못 구성하면 심각한 보안 위험이 발생할 수 있습니다.

위험한 구성

Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true는 함께 사용할 수 없습니다. 와일드카드 사용은 공개 API로만 제한하세요.

보안 CORS 구성

JavaScript (Express)CORS 구성
const cors = require('cors');

// 허용된 출처를 명시적으로 지정
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use(cors({
  origin(origin, callback) {
    // 서버 간 통신 허용(원본 없음)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,  // 비행 전 캐시: 24시간
}));

CORS 체크리스트

CORS 헤더 참조

헤더목적권장 값
Access-Control-Allow-Origin허용된 출처명시적 도메인 사양
Access-Control-Allow-Methods허용되는 HTTP 메서드최소 요구 사항만
Access-Control-Allow-Headers허용된 요청 헤더최소 요구 사항만
Access-Control-Allow-Credentials쿠키 전송 허용true (인증이 필요한 경우에만)
Access-Control-Max-Age비행 전 캐시 지속 시간(초)86400(24시간)
Access-Control-Expose-Headers자바스크립트에서 읽을 수 있는 응답 헤더RateLimit-*와 같은 필수 헤더만 표시합니다.

🤖 AI / LLM API Rate Limiting

LLM APIs introduce new dimensions to rate limiting: token consumption, cost per request, and compute-intensive inference.

Traditional API vs. LLM API Rate Limiting

DimensionTraditional APILLM API
Cost per requestLow, predictableVariable, can be 100x+ (based on tokens)
Rate limit unitRequests per time windowTokens per minute (TPM) + Requests per minute (RPM)
Abuse patternScraping, brute force, DDoSPrompt injection, resource exhaustion, denial-of-wallet
Algorithm fitFixed/Sliding windowToken bucket (weighted by token count)

Token-Aware Rate Limiting (Python)

PythonToken-Based Rate Limiter
import tiktoken
import time
from collections import defaultdict

class TokenRateLimiter:
    """Rate limiter that counts tokens, not just requests."""

    def __init__(self, tokens_per_minute=100_000, requests_per_minute=60):
        self.tpm_limit = tokens_per_minute
        self.rpm_limit = requests_per_minute
        self.usage = defaultdict(lambda: {"tokens": [], "requests": []})
        self.encoder = tiktoken.encoding_for_model("gpt-4")

    def count_tokens(self, text: str) -> int:
        return len(self.encoder.encode(text))

    def check_limit(self, user_id: str, prompt: str) -> dict:
        now = time.time()
        window = now - 60  # 1-minute sliding window
        user = self.usage[user_id]

        # Clean up expired entries
        user["tokens"] = [(t, c) for t, c in user["tokens"] if t > window]
        user["requests"] = [t for t in user["requests"] if t > window]

        # Check RPM
        if len(user["requests"]) >= self.rpm_limit:
            return {"allowed": False, "reason": "RPM limit exceeded"}

        # Check TPM
        token_count = self.count_tokens(prompt)
        used_tokens = sum(c for _, c in user["tokens"])
        if used_tokens + token_count > self.tpm_limit:
            return {"allowed": False, "reason": "TPM limit exceeded"}

        # Record usage
        user["tokens"].append((now, token_count))
        user["requests"].append(now)
        return {"allowed": True, "tokens_used": token_count}
OWASP References

Related: LLM10: Model Denial of Service, ASI04: Cascading Hallucination Attacks

🛡 Prompt Injection & AI Input Validation

Prompt injection is the #1 risk for LLM applications. Apply defense-in-depth with input validation, structural separation, and output verification.

Prompt Sanitization

Strip or escape special tokens, instruction-like patterns, and control characters from user input before including in prompts.

Structural Input Separation

Use delimiters, XML tags, or separate message roles to clearly isolate system instructions from user-provided content.

Output Verification

Validate LLM responses against expected schemas. Check for data leakage, instruction following, and malicious content before rendering.

Prompt Injection Attack Vectors & Mitigations

공격대응 방안
Direct Prompt InjectionInput sanitization + instruction/data separation"Ignore previous instructions and..."
Indirect Prompt InjectionSanitize RAG results + canary tokensMalicious instructions hidden in retrieved documents
Context StuffingToken limits + input truncationOverloading context window to push out system instructions
Parameter TamperingSchema validation + typed parametersManipulating temperature, max_tokens, or model parameters

Structured Input Validation (Python / Pydantic)

PythonPydantic Validation for LLM Requests
from pydantic import BaseModel, Field, field_validator
import re

class LLMRequest(BaseModel):
    """Validated LLM request with prompt injection defenses."""
    user_message: str = Field(..., max_length=4000)
    max_tokens: int = Field(default=1000, ge=1, le=4096)
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)

    @field_validator("user_message")
    @classmethod
    def sanitize_prompt(cls, v: str) -> str:
        # Block known injection patterns
        patterns = [
            r"(?i)ignore\s+(previous|above|all)\s+(instructions?|prompts?)",
            r"(?i)you\s+are\s+now\s+",
            r"(?i)system\s*:\s*",
            r"(?i)\[INST\]|\[\/INST\]|<\|im_start\|>",
        ]
        for pattern in patterns:
            if re.search(pattern, v):
                raise ValueError("Input contains disallowed patterns")
        return v

# Usage with FastAPI
@app.post("/api/chat")
async def chat(request: LLMRequest):
    # Separate system instructions from user input
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"<user_input>{request.user_message}</user_input>"},
    ]
    return await call_llm(messages, request.max_tokens, request.temperature)
OWASP References

Related: LLM01: Prompt Injection, LLM02: Insecure Output Handling, ASI02: Prompt Injection via Tool Results

🔒 기타 권장 보안 헤더

HTTP Response Headers권장 설정
# 콘텐츠 유형 자동 감지 비활성화
X-Content-Type-Options: nosniff

# iframe 임베딩 방지
X-Frame-Options: DENY

# HTTPS 적용
Strict-Transport-Security: max-age=31536000; includeSubDomains

# CSP: API는 JSON만 반환하므로 모든 스크립트 실행을 차단합니다.
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

# 리퍼러 정보 제한
Referrer-Policy: no-referrer

# 브라우저 기능 제한
Permissions-Policy: geolocation=(), camera=(), microphone=()