Input Validation ile API Güvenliğini Sağlama
API’lerinize gelen her isteği potansiyel bir tehdit olarak görmek biraz paranoyak gelebilir, ama bu paranoya sizi birçok baş ağrısından kurtarır. Production ortamında çalışan bir API’yi yönetiyorsanız, input validation sadece “iyi bir pratik” değil, hayatta kalma meselesidir. SQL injection, XSS, path traversal, buffer overflow… Bunların hepsi yetersiz input validation’dan beslenir. Bu yazıda, gerçek dünya senaryoları üzerinden API güvenliğini input validation ile nasıl sağlayacağınızı ele alacağız.
Input Validation Neden Bu Kadar Kritik?
Bir düşünün: Kullanıcı tarafından gelen veriyi doğrudan veritabanına, dosya sistemine veya başka bir servise gönderiyorsanız, o verinin ne içerdiğini kontrol etmiyorsunuz demektir. Bu tam anlamıyla kapınızı açık bırakmak gibi.
OWASP Top 10 listesine baktığınızda, injection saldırıları yıllardır ilk sıralarda yer alıyor. Ve bu saldırıların büyük çoğunluğu basit bir input validation mekanizmasıyla engellenebilir. Validation’ı sadece güvenlik açısından değil, API’nizin tutarlılığı ve güvenilirliği açısından da düşünmek gerekiyor.
Validation katmanınız şu soruları cevaplamalı:
- Gelen veri beklenen formatta mı?
- Veri beklenen uzunluk sınırları içinde mi?
- Veri izin verilen karakter setini kullanıyor mu?
- Veri iş mantığı kurallarını karşılıyor mu?
- Veri tipi doğru mu?
Temel Validation Stratejileri
Whitelist vs Blacklist Yaklaşımı
Blacklist yaklaşımında “şu karakterleri veya desenleri reddet” diyorsunuz. Sorun şu: Saldırganlar her zaman blacklist’inizi bypass edecek yeni yollar buluyor. Unicode encoding, double encoding, null byte injection… Bunların hepsini blacklist’e eklemeye çalışmak bir tür kedi-fare oyununa dönüşüyor.
Whitelist yaklaşımı ise “sadece şu karakterlere veya desenlere izin ver” diyor. Bu çok daha güvenli bir yöntem. “Kullanıcı adı sadece harf, rakam ve alt çizgi içerebilir” derseniz, bu seti dışındaki her şeyi otomatik olarak reddediyorsunuz.
Pratik bir Python örneğiyle başlayalım:
# Flask ile basit bir whitelist validation örneği
cat > /opt/api/validators.py << 'EOF'
import re
from functools import wraps
from flask import request, jsonify
def validate_username(username):
"""Sadece alfanumerik ve alt çizgiye izin ver, 3-30 karakter"""
if not username:
return False, "Kullanıcı adı boş olamaz"
pattern = r'^[a-zA-Z0-9_]{3,30}$'
if not re.match(pattern, username):
return False, "Kullanıcı adı 3-30 karakter, sadece harf/rakam/_ içerebilir"
return True, None
def validate_email(email):
"""RFC 5322 uyumlu email validation"""
if not email or len(email) > 254:
return False, "Geçersiz email formatı"
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
return False, "Geçersiz email formatı"
return True, None
def validate_numeric_id(value):
"""Pozitif integer ID validation"""
try:
num = int(value)
if num <= 0 or num > 2147483647:
return False, "ID 1 ile 2147483647 arasında olmalı"
return True, None
except (ValueError, TypeError):
return False, "ID sayısal olmalı"
EOF
echo "Validator modülü oluşturuldu"
Schema-Based Validation
Manuel validation yazmak bir noktadan sonra çok zahmetli hale gelir. Schema tabanlı validation kütüphaneleri bu iş için çok daha pratik.
# Python'da Pydantic ile schema validation
cat > /opt/api/schemas.py << 'EOF'
from pydantic import BaseModel, Field, validator, EmailStr
from typing import Optional, List
import re
class UserCreateSchema(BaseModel):
username: str = Field(..., min_length=3, max_length=30, regex=r'^[a-zA-Z0-9_]+$')
email: EmailStr
age: int = Field(..., ge=18, le=120)
phone: Optional[str] = None
@validator('phone')
def validate_phone(cls, v):
if v is None:
return v
# Sadece Türkiye formatı: +90XXXXXXXXXX veya 0XXXXXXXXXX
pattern = r'^(+90|0)[0-9]{10}$'
if not re.match(pattern, v):
raise ValueError('Geçersiz telefon formatı')
return v
@validator('username')
def username_not_reserved(cls, v):
reserved = ['admin', 'root', 'system', 'api', 'null', 'undefined']
if v.lower() in reserved:
raise ValueError(f'"{v}" rezerve edilmiş bir kullanıcı adı')
return v
class ProductSearchSchema(BaseModel):
query: str = Field(..., min_length=1, max_length=100)
category_id: Optional[int] = Field(None, ge=1)
min_price: Optional[float] = Field(None, ge=0)
max_price: Optional[float] = Field(None, ge=0)
page: int = Field(1, ge=1, le=1000)
per_page: int = Field(20, ge=1, le=100)
@validator('query')
def sanitize_query(cls, v):
# SQL ve script injection karakterlerini temizle
dangerous_patterns = [
r'[<>"']',
r'(union|select|insert|update|delete|drop|create)s',
r'(script|javascript|vbscript)',
r'(--|;|/*|*/)'
]
for pattern in dangerous_patterns:
if re.search(pattern, v, re.IGNORECASE):
raise ValueError('Geçersiz arama terimi')
return v.strip()
EOF
echo "Schema modülü oluşturuldu"
Nginx Seviyesinde İlk Savunma Hattı
Input validation sadece uygulama katmanında yapılmamalı. Nginx’i bir ön filtre olarak kullanmak, kötü niyetli isteklerin uygulamanıza hiç ulaşmamasını sağlar.
# /etc/nginx/conf.d/api-security.conf
cat > /etc/nginx/conf.d/api-security.conf << 'EOF'
# İstek boyutu sınırlaması
client_max_body_size 1m;
client_body_buffer_size 128k;
# Header boyutu sınırlaması
large_client_header_buffers 4 8k;
client_header_buffer_size 1k;
server {
listen 443 ssl;
server_name api.example.com;
# Uzun URI'leri reddet
if ($request_uri ~* "(.{2048,})") {
return 414;
}
# Null byte içeren istekleri reddet
if ($request_uri ~* "%00") {
return 400;
}
# Path traversal girişimlerini engelle
if ($request_uri ~* "../") {
return 403;
}
# Geçersiz HTTP metodlarını reddet
if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|OPTIONS)$) {
return 405;
}
location /api/ {
# Content-Type kontrolü
if ($request_method = POST) {
if ($content_type !~ "application/json") {
return 415;
}
}
proxy_pass http://backend:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
EOF
nginx -t && systemctl reload nginx
Middleware ile Merkezi Validation
Her endpoint’te ayrı ayrı validation yazmak hem tekrar hem de gözden kaçırma riskini artırır. Middleware yaklaşımı, validation’ı merkezi bir noktada yönetmenizi sağlar.
# Express.js (Node.js) middleware örneği
cat > /opt/api/middleware/validator.js << 'EOF'
const { body, param, query, validationResult } = require('express-validator');
// Validation hatalarını işle
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
status: 'error',
message: 'Validation hatası',
errors: errors.array().map(err => ({
field: err.param,
message: err.msg,
value: err.value
}))
});
}
next();
};
// Genel sanitization middleware
const sanitizeInput = (req, res, next) => {
// Request body'deki string alanları sanitize et
const sanitizeObject = (obj) => {
if (typeof obj !== 'object' || obj === null) return obj;
for (const key in obj) {
if (typeof obj[key] === 'string') {
// Null byte temizliği
obj[key] = obj[key].replace(//g, '');
// Başındaki/sonundaki boşlukları temizle
obj[key] = obj[key].trim();
// HTML entity encode
obj[key] = obj[key]
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
} else if (typeof obj[key] === 'object') {
obj[key] = sanitizeObject(obj[key]);
}
}
return obj;
};
if (req.body) {
req.body = sanitizeObject(req.body);
}
next();
};
// Kullanıcı oluşturma validation kuralları
const userCreateRules = [
body('username')
.isLength({ min: 3, max: 30 })
.withMessage('Kullanıcı adı 3-30 karakter olmalı')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Kullanıcı adı sadece harf, rakam ve _ içerebilir'),
body('email')
.isEmail()
.withMessage('Geçerli bir email adresi girin')
.normalizeEmail(),
body('age')
.isInt({ min: 18, max: 120 })
.withMessage('Yaş 18-120 arasında olmalı'),
handleValidationErrors
];
module.exports = { sanitizeInput, userCreateRules, handleValidationErrors };
EOF
echo "Middleware oluşturuldu"
SQL Injection’a Karşı Özel Önlemler
Input validation ve parameterized query kullanmak SQL injection’a karşı iki katmanlı koruma sağlar. Sadece ORM kullanmak yetmez; raw query yazmanız gerektiğinde ne yapacağınızı bilmeniz lazım.
# PostgreSQL ile güvenli query örnekleri
cat > /opt/api/db/safe_queries.py << 'EOF'
import psycopg2
import re
from typing import Optional, List, Dict
class SafeQueryBuilder:
ALLOWED_SORT_COLUMNS = {'id', 'username', 'email', 'created_at', 'updated_at'}
ALLOWED_SORT_ORDERS = {'ASC', 'DESC'}
def __init__(self, connection):
self.conn = connection
def get_user_by_id(self, user_id: int) -> Optional[Dict]:
"""Parameterized query ile güvenli kullanıcı sorgulama"""
# Tip kontrolü
if not isinstance(user_id, int) or user_id <= 0:
raise ValueError("Geçersiz kullanıcı ID'si")
with self.conn.cursor() as cur:
# Asla f-string veya string concatenation kullanma!
cur.execute(
"SELECT id, username, email, created_at FROM users WHERE id = %s",
(user_id,)
)
row = cur.fetchone()
if row:
return {'id': row[0], 'username': row[1], 'email': row[2]}
return None
def search_users(self, query: str, sort_by: str = 'id', order: str = 'ASC') -> List[Dict]:
"""Güvenli arama fonksiyonu"""
# Arama terimini validate et
if len(query) < 1 or len(query) > 100:
raise ValueError("Arama terimi 1-100 karakter olmalı")
# Sort column whitelist kontrolü
if sort_by not in self.ALLOWED_SORT_COLUMNS:
raise ValueError(f"Geçersiz sıralama alanı: {sort_by}")
# Sort order whitelist kontrolü
order = order.upper()
if order not in self.ALLOWED_SORT_ORDERS:
raise ValueError("Sıralama yönü ASC veya DESC olmalı")
# Sort column ve order için identifier quoting kullan
# (Bunlar parameterized olamaz, bu yüzden whitelist zorunlu)
safe_query = f"""
SELECT id, username, email
FROM users
WHERE username ILIKE %s OR email ILIKE %s
ORDER BY {sort_by} {order}
LIMIT 100
"""
search_pattern = f"%{query}%"
with self.conn.cursor() as cur:
cur.execute(safe_query, (search_pattern, search_pattern))
rows = cur.fetchall()
return [{'id': r[0], 'username': r[1], 'email': r[2]} for r in rows]
EOF
echo "Güvenli query builder oluşturuldu"
Rate Limiting ve Request Flooding Koruması
Validation’ın bir diğer boyutu da rate limiting. Bir endpoint’e saniyede binlerce istek geliyorsa, bunları validate etmek bile sunucunuzu yorabilir. Rate limiting, validation’dan önce devreye girmeli.
# Redis tabanlı rate limiting script'i
cat > /opt/api/scripts/setup_rate_limit.sh << 'EOF'
#!/bin/bash
# Redis'in kurulu olduğunu kontrol et
if ! command -v redis-cli &> /dev/null; then
echo "Redis kurulu değil, kuruluyor..."
apt-get install -y redis-server
systemctl enable --now redis-server
fi
# Nginx ile rate limiting konfigürasyonu
cat > /etc/nginx/conf.d/rate-limit.conf << 'NGINX_EOF'
# IP bazlı rate limiting zone'ları
limit_req_zone $binary_remote_addr zone=api_general:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api_upload:10m rate=5r/m;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
# ...
location /api/v1/auth/ {
limit_req zone=api_auth burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend:8000;
}
location /api/v1/upload/ {
limit_req zone=api_upload burst=2 nodelay;
limit_conn conn_limit 3;
client_max_body_size 10m;
proxy_pass http://backend:8000;
}
location /api/ {
limit_req zone=api_general burst=20 nodelay;
proxy_pass http://backend:8000;
}
}
NGINX_EOF
nginx -t && systemctl reload nginx
echo "Rate limiting konfigürasyonu tamamlandı"
EOF
chmod +x /opt/api/scripts/setup_rate_limit.sh
File Upload Validation
Dosya yükleme endpoint’leri en riskli noktalardan biri. Burada validation’ı gevşetmek web shell yüklenmesine kadar gidebilir.
# Python ile güvenli dosya upload validation
cat > /opt/api/utils/file_validator.py << 'EOF'
import os
import magic # python-magic kütüphanesi
import hashlib
from pathlib import Path
from typing import Tuple
class FileValidator:
# İzin verilen MIME type ve uzantı eşleşmeleri
ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'application/pdf': ['.pdf'],
'text/csv': ['.csv'],
}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
@classmethod
def validate_file(cls, file_path: str, original_filename: str) -> Tuple[bool, str]:
"""Kapsamlı dosya validasyonu"""
# 1. Dosya boyutu kontrolü
file_size = os.path.getsize(file_path)
if file_size > cls.MAX_FILE_SIZE:
return False, f"Dosya boyutu {cls.MAX_FILE_SIZE // (1024*1024)}MB'ı geçemez"
if file_size == 0:
return False, "Boş dosya yüklenemez"
# 2. Gerçek MIME type kontrolü (uzantıya güvenme!)
mime = magic.Magic(mime=True)
actual_mime = mime.from_file(file_path)
if actual_mime not in cls.ALLOWED_TYPES:
return False, f"İzin verilmeyen dosya tipi: {actual_mime}"
# 3. Uzantı ve MIME type eşleşme kontrolü
ext = Path(original_filename).suffix.lower()
if ext not in cls.ALLOWED_TYPES.get(actual_mime, []):
return False, "Dosya uzantısı içerikle uyuşmuyor"
# 4. Dosya adı sanitization
safe_name = cls.sanitize_filename(original_filename)
if not safe_name:
return False, "Geçersiz dosya adı"
# 5. Zararlı içerik taraması (basit imza kontrolü)
if cls._contains_php_code(file_path):
return False, "Dosya zararlı içerik barındırıyor"
return True, safe_name
@staticmethod
def sanitize_filename(filename: str) -> str:
"""Dosya adını güvenli hale getir"""
# Path separator'ları temizle
filename = os.path.basename(filename)
# Sadece güvenli karakterlere izin ver
import re
filename = re.sub(r'[^ws-.]', '', filename)
filename = filename.strip('. ')
if len(filename) > 255 or len(filename) < 1:
return None
return filename
@staticmethod
def _contains_php_code(file_path: str) -> bool:
"""Basit PHP kodu imza kontrolü"""
dangerous_patterns = [b'<?php', b'<?=', b'<script', b'eval(', b'exec(']
with open(file_path, 'rb') as f:
content = f.read(4096) # Sadece ilk 4KB'ı kontrol et
for pattern in dangerous_patterns:
if pattern.lower() in content.lower():
return True
return False
EOF
echo "Dosya validator modülü oluşturuldu"
Validation Loglarını İzleme
Validation hatalarını loglamak ve izlemek, saldırı girişimlerini tespit etmek için kritik. Hangi IP’ler, hangi endpoint’lere, nasıl girdiler gönderiyor?
# Validation log analizi için bash script
cat > /opt/api/scripts/analyze_validation_logs.sh << 'EOF'
#!/bin/bash
LOG_FILE="/var/log/api/validation_errors.log"
REPORT_FILE="/tmp/validation_report_$(date +%Y%m%d).txt"
THRESHOLD=50 # Aynı IP'den bu kadar hata gelirse uyar
echo "=== API Validation Güvenlik Raporu ===" > $REPORT_FILE
echo "Tarih: $(date)" >> $REPORT_FILE
echo "" >> $REPORT_FILE
# En fazla validation hatası üreten IP'leri bul
echo "## Top 10 Şüpheli IP Adresleri" >> $REPORT_FILE
grep "VALIDATION_ERROR" $LOG_FILE |
grep -oP 'ip=K[0-9]+.[0-9]+.[0-9]+.[0-9]+' |
sort | uniq -c | sort -rn | head -10 >> $REPORT_FILE
echo "" >> $REPORT_FILE
# Eşiği aşan IP'leri otomatik olarak engelle
echo "## Otomatik Engellenen IP'ler" >> $REPORT_FILE
grep "VALIDATION_ERROR" $LOG_FILE |
grep -oP 'ip=K[0-9]+.[0-9]+.[0-9]+.[0-9]+' |
sort | uniq -c | sort -rn |
awk -v threshold=$THRESHOLD '$1 > threshold {print $2}' |
while read ip; do
if ! iptables -L INPUT -n | grep -q "$ip"; then
iptables -A INPUT -s $ip -j DROP
echo "Engellendi: $ip" >> $REPORT_FILE
echo "IP engellendi: $ip"
fi
done
# En çok hedef alınan endpoint'ler
echo "" >> $REPORT_FILE
echo "## En Çok Saldırıya Uğrayan Endpoint'ler" >> $REPORT_FILE
grep "VALIDATION_ERROR" $LOG_FILE |
grep -oP 'endpoint=KS+' |
sort | uniq -c | sort -rn | head -10 >> $REPORT_FILE
# Raporu gönder
if command -v mail &> /dev/null; then
mail -s "API Validation Güvenlik Raporu - $(date +%Y%m%d)"
[email protected] < $REPORT_FILE
fi
echo "Rapor oluşturuldu: $REPORT_FILE"
EOF
chmod +x /opt/api/scripts/analyze_validation_logs.sh
# Cron job ekle - Her gün gece 02:00'de çalıştır
echo "0 2 * * * root /opt/api/scripts/analyze_validation_logs.sh"
> /etc/cron.d/api-validation-monitor
Gerçek Dünya Senaryosu: E-ticaret API Saldırısı
Bir e-ticaret projesinde karşılaştığım gerçek bir senaryo: Ürün arama endpoint’i, doğrulama olmadan doğrudan veritabanı sorgusuna gidiyordu. Saldırgan şu tür istekler gönderiyordu:
GET /api/products/search?q=laptop' UNION SELECT username,password,null FROM users--
Sonuç? Kullanıcı tablosu ifşa olmuştu. Çözüm için şu adımları uyguladık:
- Tüm input’lar için Pydantic şemaları yazıldı
- Nginx seviyesinde SQL pattern matching eklendi
- Parameterized query’lere geçildi
- WAF (ModSecurity) entegre edildi
- Validation hataları için merkezi loglama kuruldu
Bu olaydan sonra benzer bir isteğin geldiğinde sistem şöyle davranıyor: Nginx ilk katmanda SQL pattern’ı yakalıyor, geçerse uygulama katmanı Pydantic ile reddediyor, geçerse parameterized query saldırıyı etkisiz kılıyor. Üç katmanlı savunma.
Validation Hatalarını Doğru Yönetmek
Validation hataları kullanıcıya nasıl gösterildiği de güvenlik açısından önemli. Çok fazla bilgi vermek saldırgana yol gösterir.
Kötü bir yaklaşım şöyle görünür:
Error: Column 'users.password_hash' doesn't exist in WHERE clause
Query was: SELECT * FROM users WHERE email = 'test' AND password = 'abc'
Bu mesaj saldırgana tablo adını, kolon adını ve query yapısını söylüyor. Bunun yerine generic ve bilgilendirici ama tehdit içermeyen hatalar döndürülmeli:
{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Gönderilen veriler geçersiz",
"errors": [
{
"field": "email",
"message": "Geçerli bir email adresi girin"
}
]
}
İç log’lara ise tam detayı yazabilirsiniz, ama asla client’a göndermeyin.
Sonuç
Input validation, API güvenliğinin temel taşı. Ama dikkat edin: Validation tek başına yeterli değil. Authentication, authorization, rate limiting, encryption ve monitoring ile birlikte düşünülmesi gereken bir katman.
Pratikte şunu öneririm: Yeni bir endpoint yazarken önce şemayı, sonra kodu yazın. Schema-first yaklaşımı hem tasarımı netleştirir hem de validation’ı zorunlu kılar. Pydantic, Joi, Yup, JSON Schema gibi araçlar bu süreci dramatik biçimde hızlandırır.
Şunu da unutmayın: Client tarafında validation yapmak kullanıcı deneyimi için iyidir, ama güvenlik için hiçbir şey ifade etmez. Client-side validation bypass edilebilir. Güvenlik her zaman sunucu tarafında sağlanmalı.
Son olarak, validation mantığınızı test edin. Unit testleri yazın, boundary değerleri, boş string’ler, çok uzun string’ler, özel karakterler, Unicode karakterler, null değerler için testler ekleyin. Güvenlik testlerini CI/CD pipeline’ınıza entegre edin. Bir saldırganın deneyeceği şeyleri siz deneyin, bulmadan önce.
