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
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
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})
}