MidПрактика3 min

CORS и безопасность

Глубокое погружение в CORS, настройка WAF, rate limiting и защита бэкенда от атак

CORS: полное понимание

Как работает CORS

CORS (Cross-Origin Resource Sharing) -- это механизм безопасности браузера. Когда JavaScript на resume.ivanpetrov.com делает запрос к api.ivanpetrov.com, браузер считает это кросс-доменным запросом и проверяет, разрешает ли сервер такие запросы.

Preflight-запрос (OPTIONS)

Для "небезопасных" запросов браузер сначала отправляет preflight-запрос:

1. Браузер → OPTIONS /count HTTP/1.1
   Origin: https://resume.ivanpetrov.com
   Access-Control-Request-Method: GET

2. Сервер → HTTP/1.1 204 No Content
   Access-Control-Allow-Origin: https://resume.ivanpetrov.com
   Access-Control-Allow-Methods: GET, OPTIONS
   Access-Control-Max-Age: 86400

3. Браузер → GET /count HTTP/1.1
   Origin: https://resume.ivanpetrov.com

4. Сервер → HTTP/1.1 200 OK
   Access-Control-Allow-Origin: https://resume.ivanpetrov.com
   {"count": 42}

Когда preflight НЕ нужен (Simple Request)

Простой GET без кастомных заголовков не требует preflight:

Простой запрос (без preflight):
- Метод: GET, HEAD, POST
- Заголовки: только стандартные (Accept, Content-Type: text/plain)
- Нет ReadableStream body

Для CRC fetch(url) без дополнительных заголовков -- это simple request, и preflight не отправляется. Но API всё равно должен вернуть заголовок Access-Control-Allow-Origin.

Настройка CORS

CORS настраивается при создании/обновлении API

aws apigatewayv2 update-api
--api-id API_ID
--cors-configuration '{ "AllowOrigins": ["https://resume.ivanpetrov.com"], "AllowMethods": ["GET", "OPTIONS"], "AllowHeaders": ["Content-Type"], "ExposeHeaders": [], "MaxAge": 86400, "AllowCredentials": false }'

## WAF (Web Application Firewall)

Зачем нужен WAF

WAF фильтрует вредоносные HTTP-запросы до того, как они достигнут вашего приложения. Для CRC WAF защищает от:

  • SQL Injection (хотя мы используем NoSQL)
  • XSS в параметрах запроса
  • Bot-трафик
  • Географическая блокировка

AWS WAF

# Создание Web ACL
aws wafv2 create-web-acl \
  --name crc-waf \
  --scope CLOUDFRONT \
  --default-action '{"Allow": {}}' \
  --visibility-config '{
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "crc-waf"
  }' \
  --rules '[
    {
      "Name": "RateLimit",
      "Priority": 1,
      "Action": {"Block": {}},
      "Statement": {
        "RateBasedStatement": {
          "Limit": 100,
          "AggregateKeyType": "IP"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimit"
      }
    },
    {
      "Name": "AWSManagedRulesCommonRuleSet",
      "Priority": 2,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesCommonRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "CommonRules"
      }
    }
  ]'

Rate Limiting

На уровне API Gateway

HTTP API: встроенный throttling

aws apigatewayv2 update-stage
--api-id API_ID
--stage-name prod
--default-route-settings '{ "ThrottlingBurstLimit": 10, "ThrottlingRateLimit": 5 }'

5 запросов/секунду, burst до 10

### На уровне Lambda
import time
from collections import defaultdict

# Simple in-memory rate limiter (resets on cold start)
request_counts = defaultdict(list)

def rate_limit(ip: str, max_requests: int = 10, window: int = 60) -> bool:
    """Check if IP has exceeded rate limit."""
    now = time.time()
    # Clean old entries
    request_counts[ip] = [t for t in request_counts[ip] if now - t < window]
    # Check limit
    if len(request_counts[ip]) >= max_requests:
        return False
    request_counts[ip].append(now)
    return True

In-memory rate limiter сбрасывается при cold start Lambda. Для production используйте DynamoDB или ElastiCache.

Безопасность бэкенда

Чек-лист безопасности для CRC

Проверка Описание Статус
HTTPS Весь трафик через HTTPS Обязательно
CORS Конкретный origin, не * Рекомендуется
Rate Limiting Защита от DDoS Рекомендуется
Input Validation Валидация всех входных данных Обязательно
IAM Least Privilege Минимальные права для Lambda Обязательно
Нет секретов в коде Используйте Environment Variables Обязательно
Logging CloudWatch Logs включены Обязательно
Error Handling Не раскрывайте внутренние ошибки Обязательно

Безопасная обработка ошибок

def lambda_handler(event, context):
    try:
        count = get_and_increment()
        return success_response({'count': count})
    except Exception as e:
        # Log full error internally
        logger.error(f"Error: {e}", exc_info=True)

        # Return generic error to client (don't expose internals!)
        return error_response('Service temporarily unavailable', 503)

def error_response(message, status_code):
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': 'https://resume.ivanpetrov.com'
        },
        # НЕ включайте traceback, имена таблиц, ARN и т.д.
        'body': json.dumps({'error': message})
    }

Проверь себя

Что делает AWS Managed Rules Common Rule Set в WAF?

Что означает rate limit '5 запросов/секунду, burst до 10' на API Gateway?

Почему in-memory rate limiter в Lambda не подходит для production?

Почему нельзя возвращать подробности ошибки (traceback, имена таблиц) клиенту?

Что такое CORS preflight-запрос и когда он отправляется?