OWASP Top 10, web uygulamalarındaki en kritik risk kategorilerini sıralayan ve neredeyse her güvenlik denetiminde atıf yapılan bir farkındalık belgesi. Sorun, listenin soyut kalması: ekipler okuyor, başını sallıyor ama kendi kodunda neyi, nasıl test edeceğini bilmiyor. Bu yazıyı OWASP Top 10:2025 sürümüne göre baştan yazdık. PHP, WordPress, XenForo, Node.js veya Python ile uygulama çalıştıran küçük ekipler için her kategoride üç şey bulacaksınız: gerçekçi bir örnek, kendi başınıza yapabileceğiniz bir test ve kısa bir düzeltme kalıbı. Yazının başında 2021'den 2025'e değişiklik tablosu, sonunda ise öncelik sıralı 30 günlük kontrol listesi ve uyarlayabileceğiniz bir CI (sürekli entegrasyon) iş akışı var.
Önemli: Aşağıdaki testleri yalnızca size ait ya da yazılı test izni aldığınız sistemlerde, tercihen canlı ortamın bir kopyası olan test ortamında (staging) çalıştırın.
OWASP Top 10:2025: 2021'den bu yana ne değişti?
Güncel liste top10.owasp.org/2025 adresinde yayımlanıyor. OWASP, on kategoriden sekizini kuruluşların paylaştığı test verisinden, ikisini topluluk anketinden seçtiğini belirtiyor. Ankette en çok öne çıkan risk A03 oldu; A09 da listeye yine anket katılımcılarının oylarıyla girdi. Ekip, mümkün olan her yerde belirtiye (symptom) değil kök nedene (root cause) odaklandığını da yazıyor.| 2025 | 2021'deki karşılığı | Değişiklik |
|---|---|---|
| A01 Broken Access Control | A01 (aynı) | Yine 1. sırada; 2021'in A10'u olan SSRF bu kategoriye katıldı |
| A02 Security Misconfiguration | A05 | 5. sıradan 2. sıraya çıktı |
| A03 Software Supply Chain Failures | A06 Vulnerable and Outdated Components | Kapsam genişledi: bağımlılıklar, derleme sistemleri, dağıtım |
| A04 Cryptographic Failures | A02 | 2. sıradan 4. sıraya indi |
| A05 Injection | A03 | 3. sıradan 5. sıraya indi; XSS hâlâ bu kategoride |
| A06 Insecure Design | A04 | 4. sıradan 6. sıraya indi |
| A07 Authentication Failures | A07 Identification and Authentication Failures | Ad kısaldı, sıra aynı |
| A08 Software or Data Integrity Failures | A08 Software and Data Integrity Failures | Sıra aynı, adda küçük değişiklik |
| A09 Security Logging and Alerting Failures | A09 Security Logging and Monitoring Failures | Ad alarmı vurguluyor: log tutmak yetmez, birinin haberi olmalı |
| A10 Mishandling of Exceptional Conditions | Yok | Yeni kategori (24 CWE): yanlış hata yönetimi, hata anında açık kalma (fail open), mantık hataları |
Pratik sonuç: 2021'e göre hazırladığınız kontrol listesi çöpe gitmiyor. SSRF maddesini A01'in altına taşıyın, bağımlılık taramasını tedarik zinciri kontrolüne (A03) genişletin, hata yönetimi (A10) ve alarm üretimi (A09) için yeni maddeler ekleyin.
Küçük ekip için test araç çantası
Kategorilerde sürekli şu beş yönteme döneceğiz:- Manuel IDOR kontrolü: İki test hesabıyla birbirinin kayıtlarına erişmeyi denemek. Otomatik tarayıcıların en zayıf, insan gözünün en güçlü olduğu test budur.
- ZAP baseline taraması: Eski adıyla OWASP ZAP. Baseline modu siteyi varsayılan olarak bir dakika dolaşır (spider) ve yalnızca pasif kuralları çalıştırır; saldırı yükü göndermez.
- Bağımlılık denetimi: composer audit, npm audit ve pip-audit.
- SBOM: CycloneDX veya SPDX formatında yazılım malzeme listesi (Software Bill of Materials).
- Başlık (header) kontrolü: curl ve tarayıcının geliştirici araçları.
Bash:
# ZAP baseline: pasif tarama, HTML rapor ve kural dosyası şablonu (gen.conf)
docker run --rm -v "$(pwd)":/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://staging.example.com -g gen.conf -r zap-rapor.html
# Güvenlik başlıklarını tek komutta görmek
curl -sI https://staging.example.com | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy|x-frame-options'
A01: Bozuk erişim kontrolü (Broken Access Control)
Kategori birinci sıradaki yerini koruyor. OWASP'a göre kuruluşların paylaştığı test verisinde en sık rastlanan kategori bu ve 40 CWE'yi kapsıyor.Örnek: Bir WordPress eklentisi /wp-json/shop/v1/order/1043 gibi bir REST rotası ekliyor ve permission_callback olarak __return_true veriyor, yani rota herkese açık. ID'yi 1042 yapan herkes başkasının siparişini görüyor. Aynı ailede yalnızca menüden gizlenmiş ama adresi bilen herkesin açabildiği yönetici sayfaları, dosya yolu parametresiyle sunucudaki her dosyayı indiren uç noktalar (path traversal), CSRF koruması olmayan formlar ve fazla serbest CORS ayarları var. 2025'te SSRF (CWE-918) de burada: kullanıcının verdiği URL'yi sunucuda çeken bir "bağlantı önizleme" özelliği, 169.254.169.254 gibi bulut metadata adreslerine ya da iç servislere istek atmak için kullanılabilir.
Test (manuel IDOR kontrolü):
- Test ortamında A ve B adlı iki normal kullanıcı açın; her biriyle birer kayıt (sipariş, fatura, özel mesaj, ek dosya) oluşturun.
- A ile oturum açın ve kaydı yükleyen isteği geliştirici araçlarının Network sekmesinden "Copy as cURL" ile kopyalayın.
- Çerezi B'nin oturum çereziyle değiştirip isteği yeniden gönderin. Aynı denemeyi PUT, PATCH ve DELETE çağrıları ile JSON gövdesindeki ve gizli form alanlarındaki ID'ler için de yapın.
- Beklenen yanıt 403 veya 404. A'nın verisi 200 koduyla dönüyorsa IDOR bulgunuz var.
Bash:
# Çerez adı uygulamaya göre değişir: PHPSESSID, xf_session, wordpress_logged_in_...
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Cookie: oturum=B_KULLANICISININ_CEREZI" \
https://staging.example.com/api/faturalar/1043
Düzeltme kalıbı: Varsayılan olarak reddedin (deny by default) ve sahiplik kontrolünü sorgunun kendisine yazın. Kaydı çekip sonra if ile kontrol etmek yerine kullanıcı kimliğini WHERE koşuluna eklemek, unutulmuş bir kontrolün açık bırakacağı kapıyı kapatır:
PHP:
$stmt = $pdo->prepare('SELECT * FROM invoices WHERE id = ? AND user_id = ?');
$stmt->execute([$invoiceId, $currentUserId]);
$invoice = $stmt->fetch();
if ($invoice === false) {
http_response_code(404); // başkasının kaydı da "yok" görünür
exit;
}
A02: Güvenlik yapılandırma hataları (Security Misconfiguration)
2021'de beşinci sıradaydı, 2025'te ikinciliğe yükseldi.Örnek: Canlı sunucuda unutulmuş phpinfo.php, web'den indirilebilen .env dosyası veya .git dizini, açık kalmış hata ayıklama modu (WordPress'te WP_DEBUG ve hataları ekrana basan WP_DEBUG_DISPLAY, XenForo'da src/config.php içindeki $config['debug'], Django'da DEBUG, Flask'ta debugger), değiştirilmemiş varsayılan yönetici parolası, eksik güvenlik başlıkları.
Test: ZAP baseline eksik başlıkları ve sürüm bilgisi sızdıran sunucu başlıklarını raporlar. Açıkta kalan dosyalar için kısa bir döngü yeterli:
Bash:
for p in .env .git/HEAD phpinfo.php info.php wp-config.php.bak internal_data/ ; do
printf "%s %s\n" "$(curl -s -o /dev/null -w '%{http_code}' https://staging.example.com/$p)" "$p"
done
# Beklenen: 403 veya 404. .env ya da .git/HEAD için 200 acil bulgudur.
Düzeltme kalıbı: Ortamları betikle ve birbirinin aynısı olacak şekilde kurun, kullanılmayan bileşenleri kaldırın, güvenlik başlıklarını sunucu seviyesinde verin. Nginx örneği:
Kod:
# HSTS: önce kısa süre; sorun çıkmazsa max-age=31536000
add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
- Nginx'te bir location bloğu kendi add_header satırlarını tanımlarsa, varsayılan davranışta üst seviyedeki başlıklar o blokta kaybolur. Nginx 1.29.3 ve sonrasında üst seviyeye add_header_inherit merge; yazarsanız alt bloklar bu başlıkları kendi başlıklarına ekler. Önemli yolları curl ile ayrı ayrı kontrol edin; uygulama aynı başlığı zaten gönderiyorsa çift başlık oluşmasın.
- HSTS'de includeSubDomains'i yalnızca tüm alt alan adlarınız HTTPS sunuyorsa ekleyin. Preload için hstspreload.org en az 31536000 saniye, includeSubDomains ve preload ister; listeden çıkmak mümkün ama değişikliğin kullanıcılara ulaşması aylar sürer.
- CSP'yi önce Report-Only modunda yayınlayın ve ihlalleri tarayıcı konsolunda izleyin. Report-Only modu hiçbir şeyi engellemez, frame-ancestors da buna dahil; bu yüzden geçiş döneminde X-Frame-Options satırını tutun. WordPress ve XenForo'da eklenti ve temalardaki satır içi (inline) betikler yüzünden sıkı bir politika emek ister; object-src 'none', base-uri 'self' ve frame-ancestors gibi düşük riskli yönergelerle başlayın. Express'te helmet paketi, Django 6.0 ve sonrasında yerleşik SECURE_CSP ayarı işi kolaylaştırır.
A03: Yazılım tedarik zinciri hataları (Software Supply Chain Failures)
2021'deki "Vulnerable and Outdated Components" maddesinin genişletilmiş hali: yalnızca eski kütüphaneler değil, derleme sistemleri, CI/CD ve dağıtım süreci de kapsamda.Örnek: Resmî olmayan bir siteden indirilmiş "nulled" WordPress teması veya XenForo eklentisi; yıllardır güncellenmeyen bir eklenti; kurulum betiğiyle (postinstall) geliştirici token'larını çalan bir npm paketi. OWASP bu kategoride 2025'teki Shai-Hulud npm solucanını örnek veriyor: zararlı paket sürümleri kurulum betikleriyle veri çalmış, ele geçirilen npm token'larıyla da yeni zararlı sürümler yayımlamış.
Test: Her ekosistemin kendi denetim komutu var ve üçü de bulgu olduğunda sıfırdan farklı bir kodla çıktığı için CI'da doğrudan kullanılabilir:
Bash:
composer audit --locked --no-dev # audit komutu Composer 2.4 ile geldi
npm audit --audit-level=high # yalnızca high ve critical bulgularda hata verir
npm audit signatures # kayıt imzalarını ve provenance bilgisini doğrular
pip-audit -r requirements.txt # kurulum: pip install pip-audit
# SBOM (CycloneDX veya SPDX)
npm sbom --sbom-format cyclonedx > sbom-npm.cdx.json
cyclonedx-py requirements requirements.txt -o sbom-py.cdx.json
syft . -o cyclonedx-json=sbom.cdx.json -o spdx-json=sbom.spdx.json
Düzeltme kalıbı:
- Kilit dosyalarını (composer.lock, package-lock.json, hash'li requirements.txt) depoya ekleyin; canlıda composer install --no-dev, npm ci ve pip install --require-hashes kullanın.
- Bileşenleri yalnızca resmî kaynaklardan alın; nulled tema ve eklenti kullanmayın, terk edilmiş (abandoned) paketleri değiştirin.
- Yeni yayımlanan sürümleri bir süre bekletin: npm 11.10 ve sonrasında min-release-age (gün), pnpm'de minimumReleaseAge (dakika) ayarı var. Bağımlılıkların kurulum betiklerini de denetim altında tutun: npm 12 ve sonrası ile güncel pnpm sürümleri bu betikleri varsayılan olarak engelliyor ve yalnızca onayladığınız paketlerde çalıştırıyor (npm'de package.json içindeki allowScripts alanı ve npm approve-scripts komutu, pnpm'de allowBuilds). Daha eski npm sürümlerinde gerekmeyen betikleri ignore-scripts ile kapatın.
- npm, GitHub ve paket kayıtlarındaki hesaplarda MFA zorunlu olsun; CI ortamı, dağıtım yaptığı sistemlerden daha zayıf korunmasın.
- Her sürümün SBOM'unu saklayın. Bir sonraki büyük açık duyurulduğunda "bu kütüphane bizde var mı, hangi sürümde?" sorusunu dakikalar içinde yanıtlarsınız; OWASP Dependency-Track bu listeleri sürekli izleyebilir.
A04: Kriptografik hatalar (Cryptographic Failures)
Örnek: Eski bir üyelik tablosunda MD5 ile saklanan parolalar, HTTP'ye düşürülebilen (downgrade) giriş sayfası, depoya girmiş API anahtarları, tahmin edilebilir oturum veya parola sıfırlama token'ları.Test: Veritabanındaki parola sütununa bakın: $2y$ (veya $2b$) ile başlayan değer bcrypt, $argon2id$ ile başlayan değer Argon2id hash'idir; 32 karakterlik onaltılık (hex) değer büyük olasılıkla MD5, 40 karakterlik olan SHA-1'dir. WordPress'te $P$ ile başlayan değerler 6.8 öncesinin phpass hash'leridir; 6.8 ve sonrası $wp$2y$ önekiyle bcrypt kullanır ve eski hash'ler kullanıcı giriş yaptıkça yenilenir. TLS yapılandırmasını testssl.sh veya SSL Labs ile, depo geçmişindeki sırları gitleaks git . komutuyla tarayın.
Düzeltme kalıbı: OWASP'ın parola saklama rehberi (Password Storage Cheat Sheet) en az 19 MiB bellek, 2 iterasyon ve paralellik 1 ayarlarıyla Argon2id algoritmasını öneriyor; bcrypt eski sistemlerde en az 10 maliyet değeri ve 72 baytlık girdi sınırı gözetilerek kabul görüyor. Kendi algoritmanızı yazmayın:
PHP:
$hash = password_hash($password, PASSWORD_ARGON2ID); // PHP Argon2 desteğiyle derlenmişse
// Girişte: doğrula, parametreler değiştiyse yeniden hash'le
if (password_verify($password, $stored) && password_needs_rehash($stored, PASSWORD_ARGON2ID)) {
$newHash = password_hash($password, PASSWORD_ARGON2ID); // veritabanına kaydedin
}
$token = bin2hex(random_bytes(32)); // sıfırlama ve API token'ları için
A05: Enjeksiyon (Injection)
Örnek: Özel bir arama filtresinde ' OR '1'='1 girdisiyle tüm kayıtları döndüren, metin birleştirmeyle kurulmuş SQL; kullanıcının seçtiği sütun adıyla oluşturulan ORDER BY; yorumlarda kaçışlanmamış (escape edilmemiş) çıktıdan doğan XSS; dosya adını doğrudan kabuk komutuna ekleyen bir görsel dönüştürme betiği. NoSQL, LDAP ve şablon (template) enjeksiyonu da bu ailede. LLM tabanlı özelliklerde OWASP, prompt injection için ayrı yayımladığı OWASP LLM Top 10'a yönlendiriyor.Test: Parametrelere tek ve çift tırnak ekleyin; SQL hata mesajı ya da beklenmedik sonuç bulgudur. Kod tarafında sorgu metnine değişken eklenen satırları grep ile arayın. sqlmap gibi saldırı araçlarını yalnızca test izniniz olan ortamlarda kullanın.
Düzeltme kalıbı: Parametreli sorgu (prepared statement); her yığında aynı fikir:
PHP:
// PDO
$stmt = $pdo->prepare('SELECT * FROM posts WHERE author_id = ? AND status = ?');
$stmt->execute([$authorId, 'published']);
// WordPress: %d tam sayı, %s metin, %i tablo/sütun adı (%i için WordPress 6.2+)
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders WHERE customer_id = %d", $customerId));
// XenForo eklentisi
$user = \XF::db()->fetchRow('SELECT * FROM xf_user WHERE user_id = ?', [$userId]);
Kod:
// Node.js (node-postgres)
const { rows } = await pool.query(
'SELECT * FROM orders WHERE id = $1 AND user_id = $2', [orderId, userId]);
Python:
# Python (psycopg); sqlite3'te yer tutucu ? işaretidir
cur.execute("SELECT * FROM orders WHERE id = %s AND user_id = %s", (order_id, user_id))
A06: Güvensiz tasarım (Insecure Design)
Bu kategori, kod doğru yazılsa bile tasarımda hiç düşünülmemiş kontrollerin eksikliğini anlatır. OWASP ayrımı net koyuyor: kusursuz kodlama, tasarımda hiç yer almamış bir savunmayı yerine koyamaz.Örnek: Tek kullanımlık indirim kodu, aynı anda gönderilen on istekle on kez uygulanıyor. Altı haneli parola sıfırlama kodunda deneme sınırı yok. Ödeme adımı atlanıp doğrudan "sipariş tamamlandı" adresine gidilebiliyor. Parola sıfırlama, bağlantı yerine parolanın kendisini e-postayla gönderiyor.
Test: Her kritik akış için kötüye kullanım senaryoları (misuse cases) yazın ve deneyin; adımları atlayın, sırayı değiştirin, aynı isteği paralel gönderin:
Bash:
for n in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Cookie: oturum=TEST_CEREZI" \
-d "code=HOSGELDIN10" https://staging.example.com/sepet/kupon &
done; wait
# Yalnızca bir istek başarılı olmalı.
Düzeltme kalıbı: Kuralı veritabanında atomik hale getirin: UPDATE coupons SET used = used + 1 WHERE id = ? AND used < max_uses sorgusundan sonra etkilenen satır sayısı 0 ise kuponu reddedin; kullanıcı başına tek kullanım için (coupon_id, user_id) üzerinde benzersiz kısıt (unique constraint) tanımlayın. Çok adımlı akışları sunucu tarafında bir durum makinesiyle yönetin, OTP ve sıfırlama kodlarında deneme sayısını ve geçerlilik süresini sınırlayın. Önemli her özellik için yarım saatlik bir tehdit modelleme oturumu iyi bir başlangıçtır: "Bu özelliği kim, nasıl kötüye kullanır?"
A07: Kimlik doğrulama hataları (Authentication Failures)
Örnek: wp-login.php veya forum giriş sayfasına sızdırılmış kullanıcı adı ve parola listeleriyle toplu deneme (credential stuffing); girişten sonra değişmeyen oturum kimliği (session fixation); çıkışta sunucuda geçersiz kılınmayan oturum; "kullanıcı bulunamadı" ve "parola yanlış" için farklı mesajlarla kullanıcı adı keşfi (enumeration).Test: Test hesabına 20 kez yanlış parola girin; yavaşlatma, geçici kilit veya CAPTCHA devreye girmeli. Var olan ve olmayan kullanıcı için hata mesajlarını karşılaştırın. Girişten önce ve sonra oturum çerezinin değerini not edin; değişmiş olmalı. Çıkış yaptıktan sonra eski çerezi curl ile yeniden gönderin; reddedilmeli. Çerezlerdeki Secure, HttpOnly ve SameSite işaretlerini geliştirici araçlarından kontrol edin.
Düzeltme kalıbı:
INI:
; php.ini: oturum çerezleri
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1
A08: Yazılım veya veri bütünlüğü hataları (Software or Data Integrity Failures)
Örnek: Çerezdeki veriyi PHP unserialize() ile açan bir eklenti; CDN'den bütünlük kontrolü olmadan yüklenen bir JavaScript dosyası; imzası doğrulanmayan bir webhook çağrısıyla "ödendi" durumuna geçen sipariş; HTTP üzerinden zip indirip açan, kendi yazdığınız bir otomatik güncelleyici.Test: Kod tabanında unserialize(, pickle.loads ve yaml.load( çağrılarını arayın, girdinin nereden geldiğine bakın. WordPress'te wp core verify-checksums ve wp plugin verify-checksums --all komutları dosyaları WordPress.org'daki sağlama toplamlarıyla (checksum) karşılaştırır; ikincisi yalnızca WordPress.org dizinindeki eklentiler için çalışır. Sayfa kaynağında harici betik etiketlerinin integrity özniteliği taşıyıp taşımadığına bakın.
Düzeltme kalıbı: İstemciden gelen her veride dilin yerel serileştirmesi yerine JSON kullanın (PHP'de json_decode ve json_encode). PHP kılavuzu bu konuda net: güvenilmeyen girdiyi, allowed_classes seçeneği verilmiş olsa bile unserialize() ile açmayın. Önbellek veya eski bir tablo gibi sizin sakladığınız serileştirilmiş veriyi açmanız gerekiyorsa önce hash_hmac ile doğrulayın, ek önlem olarak da ['allowed_classes' => false] verin. Python'da YAML için yaml.safe_load kullanın, güvenilmeyen veriyi asla pickle ile açmayın. İstemciye durum (state) taşımanız gerekiyorsa veriyi HMAC ile imzalayıp sunucuda doğrulayın. Webhook alıcılarında imza, zaman damgası ve tekrar (replay) korumasını güvenli webhook alıcısı yazımızda ayrıntılı anlattık. CDN dosyaları için Subresource Integrity (SRI) hash'ini üretip betik etiketine integrity="sha384-..." ve crossorigin="anonymous" öznitelikleriyle ekleyin:
Bash:
openssl dgst -sha384 -binary kutuphane.min.js | openssl base64 -A
A09: Güvenlik loglama ve alarm hataları (Security Logging and Alerting Failures)
OWASP, kategorinin adını değiştirirken alarmı öne çıkarıyor: kimsenin bakmadığı log, saldırıyı yakalamaz.Örnek: Gece 03.00'te yeni bir yönetici hesabı açılıyor ve kimse fark etmiyor; loglar yalnızca ele geçirilen sunucunun kendi diskinde durduğu için saldırgan onları siliyor.
Test: Test ortamında 10 başarısız giriş yapın ve bir test yönetici hesabı oluşturun. Bu olaylar zaman damgası, IP ve kullanıcı adıyla loglandı mı? Belirlediğiniz süre içinde bir insana alarm ulaştı mı? İkinci sorunun cevabı "hayır" ise A09 bulgunuz var.
Düzeltme kalıbı:
- Şunları loglayın: başarılı ve başarısız girişler, erişim reddi (403) olayları, sunucu tarafı doğrulama hataları, rol ve yetki değişiklikleri, parola ve MFA değişiklikleri, yapılandırma değişiklikleri, toplu veri dışa aktarımları.
- Logları JSON gibi makinenin işleyebileceği bir formatta yazın; kullanıcı girdisindeki satır sonu karakterlerini kodlayarak log enjeksiyonunu önleyin.
- Parola, token ve kart numarası loglamayın; kişisel veriyi en aza indirin. Saklama süreleri ve erişim yetkileri KVKK açısından da değerlendirilmeli; güncel yükümlülükleri resmî kaynaktan kontrol edin.
- Logları sunucunun dışına gönderin: merkezi bir log sunucusu, Loki ve Grafana, ELK veya OpenSearch ya da barındırılan bir servis.
- Üç ila beş net alarm kuralıyla başlayın: dakikada belirli bir sayının üzerinde başarısız giriş, yeni yönetici hesabı, yönetici girişinde yeni ülke veya IP, 5xx hatalarında ani artış.
A10: İstisnai durumların hatalı yönetimi (Mishandling of Exceptional Conditions)
2025'in yeni kategorisi (24 CWE). Uygulamanın olağan dışı durumları önleyememesini, fark edememesini ya da bunlara yanlış tepki vermesini kapsıyor.Örnek: Veritabanı hatasında SQL sorgusunu ve dosya yollarını gösteren hata sayfası; yetki servisi zaman aşımına uğradığında catch bloğunun isteği geçirmesi (fail open); para transferinde borç kaydı yazılıp alacak kaydı yazılmadan işlemin kesilmesi; yükleme hatasında geçici dosyaların ve bağlantıların serbest bırakılmaması.
Test: Beklenmeyen girdi gönderin: eksik parametre, yanlış tip (PHP'de ?id[]=1 gibi dizi), bozuk JSON, çok büyük istek gövdesi. Test ortamında veritabanını veya bağımlı bir servisi durdurun. Yanıtta yığın izi (stack trace), sorgu, sürüm ya da dosya yolu görünmemeli; yetki gerektiren işlemler reddedilmeli; olay log'da bir korelasyon kimliğiyle (correlation ID) yer almalı.
Düzeltme kalıbı:
INI:
; php.ini (canlı ortam)
display_errors = Off
log_errors = On
expose_php = Off
PHP:
set_exception_handler(function (Throwable $e) {
$id = bin2hex(random_bytes(8));
error_log("[$id] " . $e); // ayrıntı yalnızca sunucu logunda
http_response_code(500);
echo "Beklenmeyen bir hata oluştu. Destek kodu: $id";
});
try {
$allowed = $authz->canEdit($user, $post);
} catch (Throwable $e) {
error_log((string) $e);
$allowed = false; // fail closed: hata anında reddet
}
$pdo->beginTransaction(); // PHP 8'de PDO varsayılan olarak istisna fırlatır
try {
$debit->execute([$amount, $fromId]);
$credit->execute([$amount, $toId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack(); // yarım işlem bırakma
throw $e;
}
Öncelik sıralı 30 günlük kontrol listesi
- 1-3. gün: Varlık listesi çıkarın (alan adları, uygulamalar, yönetici panelleri, eklentiler). Çekirdek, tema, eklenti ve paketleri güncelleyin; yönetici hesaplarında MFA'yı açın; varsayılan parolaları değiştirin.
- 4-7. gün: Hata ayıklama modlarını kapatın, açıkta kalan dosya testini çalıştırın, .env ve .git erişimini engelleyin, bir yedeği gerçekten geri yükleyerek yedeklerin çalıştığını doğrulayın.
- 8-12. gün: composer audit, npm audit ve pip-audit çalıştırın; critical ve high bulguları kapatın; terk edilmiş ve nulled bileşenleri kaldırın; ilk SBOM'u üretin.
- 13-16. gün: gitleaks ile depo geçmişini tarayın, bulunan sırları yenileyin. Parola hash formatını kontrol edin, gerekiyorsa girişte yeniden hash'lemeyi planlayın.
- 17-21. gün: En çok kullanılan on uç noktada manuel IDOR testi yapın; HSTS'yi kısa max-age ile, CSP'yi Report-Only modunda yayınlayın; çerez işaretlerini düzeltin.
- 22-25. gün: Giriş ve parola sıfırlama formlarında istek sınırlamayı (rate limiting) test edin; log olaylarını tamamlayın ve ilk üç alarm kuralını kurun.
- 26-28. gün: Hata sayfalarını ve fail closed davranışını test edin; global hata yakalayıcıyı devreye alın.
- 29-30. gün: Aşağıdaki CI iş akışını ekleyin, haftalık zamanlanmış çalışmayı açın ve sonraki çeyrek için tehdit modelleme oturumlarını takvime koyun.
CI'da otomatik güvenlik kontrolleri
Aşağıdaki GitHub Actions iş akışı bağımlılık denetimini, SBOM üretimini ve test ortamına ZAP baseline taramasını birleştiriyor. Kullanmadığınız ekosistemin adımlarını silin; komutların aynısı GitLab CI veya Jenkins'te de çalışır. Haftalık zamanlama önemli: kodunuz değişmese de bağımlılıklarınız için yeni açıklar yayımlanır.
YAML:
name: guvenlik
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "0 5 * * 1" # her pazartesi 05:00 (UTC)
permissions:
contents: read
jobs:
bagimliliklar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
- run: composer audit --locked --no-dev
- uses: actions/setup-node@v7
with:
node-version: "lts/*"
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=high
- uses: actions/setup-python@v7
with:
python-version: "3.x"
- run: pip install pip-audit && pip-audit -r requirements.txt
- uses: anchore/sbom-action@v0 # SBOM'u iş akışı çıktısı (artifact) olarak yükler
with:
format: cyclonedx-json
zap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: zaproxy/[email protected]
with:
target: "https://staging.example.com"
rules_file_name: ".zap/rules.tsv"
cmd_options: "-I"
fail_action: true
allow_issue_writing: false
Kod:
10035 FAIL (Strict-Transport-Security Header)
10021 FAIL (X-Content-Type-Options Header Missing)
90022 FAIL (Application Error Disclosure)
10038 WARN (Content Security Policy (CSP) Header Not Set)
Sık sorulan sorular
2021 listesine göre hazırladığımız kontrol listesi geçersiz mi oldu?Hayır. Kategorilerin çoğu yerinde duruyor; SSRF'yi A01'e taşıyın, bileşen taramasını tedarik zinciri kontrolüne genişletin, A10 ve alarm maddelerini ekleyin.
SSRF listeden çıktı mı?
Hayır, artık ayrı bir başlık değil, A01'in içinde (CWE-918). Kullanıcının verdiği URL'yi sunucuda çeken her özellik hâlâ test edilmeli.
ZAP baseline canlı siteye zarar verir mi?
Baseline pasif kurallar çalıştırır ve saldırı göndermez, ancak siteyi dolaşırken istek üretir; yoğun sitelerde trafiği izleyin. Gerçek saldırı denemeleri yapan tam tarama (zap-full-scan.py) yalnızca test ortamında çalıştırılmalı.
Kod yazmıyoruz, WordPress ve XenForo kullanıyoruz. Bu liste bize de uyar mı?
Uyar. Riskin büyük kısmı A02 (yapılandırma), A03 (eklenti ve tema), A07 (giriş ve MFA) ve A09'da (alarm) toplanır. Özel kod veya eklenti eklediğiniz anda A01 ve A05 de devreye girer.
OWASP Top 10'a uymak yeterli mi?
Top 10 bir farkındalık belgesi, denetim standardı değil. Uyumluluk gerekli ama güvenlikle aynı şey değil; doğrulanabilir gereksinimler için OWASP ASVS'yi kullanın.
Sonuç ve sonraki adımlar
Bu hafta üç işe odaklanın: en kritik uç noktalarınızda manuel IDOR testi, güvenlik başlıkları kontrolü ve bağımlılık denetimi. Ardından 30 günlük listeyi izleyip kontrolleri CI iş akışıyla kalıcı hale getirin. Sunucu katmanı için canlı Linux sunucuları sıkılaştırma yazımızla devam edebilirsiniz; dışarıdan bir değerlendirme isterseniz AIOR'un siber güvenlik hizmetlerine göz atabilirsiniz. Sizin yığınınızda en çok atlanan kontrol hangisi? Deneyiminizi bu konuya yazın.The OWASP Top 10 is the awareness document that almost every security review cites. It is also abstract enough that teams read it, nod, and never turn it into tests for their own code. We have rewritten this guide for the OWASP Top 10:2025. It is aimed at small teams running PHP, WordPress, XenForo, Node.js or Python, and each category gets three things: a realistic example, a test you can run yourself and a short fix pattern. A 2021-to-2025 change table comes first; a prioritised 30-day checklist and a CI workflow you can adapt close the guide.
Important: run these tests only against systems you own or have written permission to test, ideally a staging copy of production.
OWASP Top 10:2025: what changed since 2021?
The current list is published at top10.owasp.org/2025. OWASP says it picked eight of the ten categories from contributed testing data and two from a community survey. A03 was the survey's top-ranked concern, and A09 was again voted into the list by survey participants. The authors also say they focused on root causes rather than symptoms wherever they could.| 2025 | 2021 equivalent | What changed |
|---|---|---|
| A01 Broken Access Control | A01 (same) | Still #1; SSRF, which was A10 in 2021, is now part of it |
| A02 Security Misconfiguration | A05 | Up from #5 to #2 |
| A03 Software Supply Chain Failures | A06 Vulnerable and Outdated Components | Wider scope: dependencies, build systems, distribution |
| A04 Cryptographic Failures | A02 | Down from #2 to #4 |
| A05 Injection | A03 | Down from #3 to #5; XSS still lives here |
| A06 Insecure Design | A04 | Down from #4 to #6 |
| A07 Authentication Failures | A07 Identification and Authentication Failures | Shorter name, same rank |
| A08 Software or Data Integrity Failures | A08 Software and Data Integrity Failures | Same rank, small change to the name |
| A09 Security Logging and Alerting Failures | A09 Security Logging and Monitoring Failures | Renamed to stress alerting: logs need someone to act on them |
| A10 Mishandling of Exceptional Conditions | None | New (24 CWEs): poor error handling, failing open, logic errors |
The practical takeaway: a checklist built on the 2021 list is not obsolete. Move SSRF under A01, widen dependency scanning into supply-chain control (A03), and add items for error handling (A10) and alerting (A09).
The small-team testing toolkit
Five methods come up again and again below:- Manual IDOR check: two test accounts trying to reach each other's records. Automated scanners are weakest here and a human tester is strongest.
- ZAP baseline scan: ZAP was formerly known as OWASP ZAP. Baseline mode spiders the site for one minute by default and runs passive rules only; it sends no attack payloads.
- Dependency audits: composer audit, npm audit and pip-audit.
- SBOM: a Software Bill of Materials in CycloneDX or SPDX format.
- Header checks: curl and the browser's developer tools.
Bash:
# ZAP baseline: passive scan, HTML report and a rules template (gen.conf)
docker run --rm -v "$(pwd)":/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://staging.example.com -g gen.conf -r zap-report.html
# See the security headers in one command
curl -sI https://staging.example.com | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy|x-frame-options'
A01: Broken Access Control
The category keeps the top spot. OWASP reports that it has the most occurrences in the contributed testing data, and it maps 40 CWEs.Example: a WordPress plugin registers a REST route such as /wp-json/shop/v1/order/1043 with __return_true as its permission_callback, so the route is public. Anyone who changes the ID to 1042 sees someone else's order. The same family includes admin pages that are merely hidden from the menu but open to anyone who knows the URL, download endpoints that serve any file on the server through a path parameter (path traversal), forms without CSRF protection and over-permissive CORS. SSRF (CWE-918) lives here in 2025 too: a "link preview" feature that fetches a user-supplied URL on the server can be pointed at cloud metadata addresses such as 169.254.169.254 or at internal services.
Test (manual IDOR check):
- On staging, create two ordinary users, A and B, and give each one a record (an order, invoice, private message or attachment).
- Log in as A and copy the request that loads A's record with "Copy as cURL" in the Network tab of the developer tools.
- Swap in B's session cookie and replay the request. Repeat for PUT, PATCH and DELETE calls, and for IDs inside JSON bodies and hidden form fields.
- The expected answer is 403 or 404. A 200 carrying A's data is an IDOR finding.
Bash:
# The cookie name depends on the app: PHPSESSID, xf_session, wordpress_logged_in_...
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Cookie: session=USER_B_SESSION_COOKIE" \
https://staging.example.com/api/invoices/1043
Fix pattern: deny by default and put ownership into the query itself. Adding the user ID to the WHERE clause, instead of loading the record and checking it afterwards with an if, closes the hole a forgotten check would leave open:
PHP:
$stmt = $pdo->prepare('SELECT * FROM invoices WHERE id = ? AND user_id = ?');
$stmt->execute([$invoiceId, $currentUserId]);
$invoice = $stmt->fetch();
if ($invoice === false) {
http_response_code(404); // someone else's record also looks "not found"
exit;
}
A02: Security Misconfiguration
It was fifth in 2021 and is second in 2025.Example: a forgotten phpinfo.php on the live server, a downloadable .env file or .git directory, debug mode left on (WP_DEBUG plus WP_DEBUG_DISPLAY in WordPress, $config['debug'] in XenForo's src/config.php, DEBUG in Django, the Flask debugger), an unchanged default admin password, missing security headers.
Test: the ZAP baseline reports missing headers and server headers that leak version information. A short loop covers exposed files:
Bash:
for p in .env .git/HEAD phpinfo.php info.php wp-config.php.bak internal_data/ ; do
printf "%s %s\n" "$(curl -s -o /dev/null -w '%{http_code}' https://staging.example.com/$p)" "$p"
done
# Expected: 403 or 404. A 200 for .env or .git/HEAD is an urgent finding.
Fix pattern: build environments from scripts so they stay identical, remove what you don't use, and send security headers at the server level. An Nginx example:
Kod:
# HSTS: start short; raise to max-age=31536000 once nothing breaks
add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
- If an Nginx location block defines its own add_header lines, by default it drops the headers set at the higher level. On Nginx 1.29.3 and later, add_header_inherit merge; at the top level makes nested blocks add the inherited headers to their own. Check important paths separately with curl, and make sure you don't send a header twice when the application already sets it.
- Add includeSubDomains to HSTS only when every subdomain serves HTTPS. For preloading, hstspreload.org requires a max-age of at least 31536000 seconds plus includeSubDomains and preload; removal is possible, but it takes months to reach users.
- Ship CSP in Report-Only mode first and watch violations in the browser console. Report-Only mode blocks nothing, frame-ancestors included, so keep the X-Frame-Options line during the transition. On WordPress and XenForo, inline scripts from plugins and themes make a strict policy real work; start with low-risk directives such as object-src 'none', base-uri 'self' and frame-ancestors. The helmet package for Express and the built-in SECURE_CSP setting in Django 6.0 and later make this easier.
A03: Software Supply Chain Failures
This is the 2021 "Vulnerable and Outdated Components" item, widened: build systems, CI/CD and distribution now count as well as old libraries.Example: a "nulled" WordPress theme or XenForo add-on downloaded from an unofficial site; a plugin nobody has updated in years; an npm package whose install script (postinstall) steals developer tokens. OWASP cites the 2025 Shai-Hulud npm worm here: malicious package versions stole data through install scripts and used stolen npm tokens to publish further malicious versions.
Test: each ecosystem has its own audit command, and all three exit non-zero when they find something, so they drop straight into CI:
Bash:
composer audit --locked --no-dev # the audit command arrived in Composer 2.4
npm audit --audit-level=high # fails only on high and critical findings
npm audit signatures # verifies registry signatures and provenance
pip-audit -r requirements.txt # install with: pip install pip-audit
# SBOM (CycloneDX or SPDX)
npm sbom --sbom-format cyclonedx > sbom-npm.cdx.json
cyclonedx-py requirements requirements.txt -o sbom-py.cdx.json
syft . -o cyclonedx-json=sbom.cdx.json -o spdx-json=sbom.spdx.json
Fix pattern:
- Commit lock files (composer.lock, package-lock.json, a hashed requirements.txt) and deploy with composer install --no-dev, npm ci and pip install --require-hashes.
- Take components only from official sources: no nulled themes or add-ons, and replace abandoned packages.
- Let new releases age before you install them: npm 11.10 and later have min-release-age (in days) and pnpm has minimumReleaseAge (in minutes). Keep dependency install scripts under control as well: npm 12 and later, like current pnpm versions, block them by default and run them only for packages you approve (the allowScripts field in package.json, managed with npm approve-scripts; allowBuilds in pnpm). On older npm versions, switch off the scripts you don't need with ignore-scripts.
- Require MFA on npm, GitHub and registry accounts, and don't protect CI less well than the systems it deploys to.
- Keep an SBOM for every release. When the next big vulnerability is announced, "do we use this library, and which version?" takes minutes to answer; OWASP Dependency-Track can watch those SBOMs continuously.
A04: Cryptographic Failures
Example: MD5 password hashes in an old membership table, a login page that can be downgraded to HTTP, API keys committed to the repository, predictable session or password-reset tokens.Test: look at the password column: values starting with $2y$ (or $2b$) are bcrypt and $argon2id$ marks Argon2id; a 32-character hex value is most likely MD5 and a 40-character one SHA-1. In WordPress, values starting with $P$ are phpass hashes from before 6.8; since 6.8, WordPress uses bcrypt with a $wp$2y$ prefix and rehashes old passwords as users log in. Check TLS with testssl.sh or SSL Labs, and run gitleaks git . in the repository root to scan its history for secrets.
Fix pattern: OWASP's Password Storage Cheat Sheet recommends Argon2id with at least 19 MiB of memory, 2 iterations and a parallelism of 1; bcrypt remains acceptable for legacy systems with a work factor of 10 or more, keeping its 72-byte input limit in mind. Don't roll your own:
PHP:
$hash = password_hash($password, PASSWORD_ARGON2ID); // if PHP was built with Argon2
// At login: verify, then rehash when the parameters have changed
if (password_verify($password, $stored) && password_needs_rehash($stored, PASSWORD_ARGON2ID)) {
$newHash = password_hash($password, PASSWORD_ARGON2ID); // save it to the database
}
$token = bin2hex(random_bytes(32)); // reset and API tokens
A05: Injection
Example: a custom search filter built by string concatenation that returns every row for the input ' OR '1'='1; an ORDER BY built from a user-chosen column name; XSS from unescaped output in comments; an image conversion script that pastes the file name straight into a shell command. NoSQL, LDAP and template injection belong to the same family. For LLM features, OWASP points to its separate OWASP LLM Top 10 for prompt injection.Test: add single and double quotes to parameters; an SQL error or an unexpected result is a finding. Grep the code for queries assembled from variables. Use offensive tools such as sqlmap only on a staging system you are authorised to test.
Fix pattern: parameterised queries (prepared statements), the same idea in every stack:
PHP:
// PDO
$stmt = $pdo->prepare('SELECT * FROM posts WHERE author_id = ? AND status = ?');
$stmt->execute([$authorId, 'published']);
// WordPress: %d integer, %s string, %i table/column name (%i needs WordPress 6.2+)
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders WHERE customer_id = %d", $customerId));
// XenForo add-on
$user = \XF::db()->fetchRow('SELECT * FROM xf_user WHERE user_id = ?', [$userId]);
Kod:
// Node.js (node-postgres)
const { rows } = await pool.query(
'SELECT * FROM orders WHERE id = $1 AND user_id = $2', [orderId, userId]);
Python:
# Python (psycopg); sqlite3 uses ? as the placeholder
cur.execute("SELECT * FROM orders WHERE id = %s AND user_id = %s", (order_id, user_id))
A06: Insecure Design
This category is about controls that were never designed in, so it can exist even when every line of code is correct. OWASP draws the line clearly: a perfect implementation cannot add a defence the design never included.Example: a single-use discount code is applied ten times by ten simultaneous requests. A six-digit password-reset code has no attempt limit. The payment step can be skipped by going straight to the "order complete" URL. Password reset emails the password itself instead of a link.
Test: write misuse cases for every critical flow and try them: skip steps, change their order, send the same request in parallel:
Bash:
for n in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Cookie: session=TEST_COOKIE" \
-d "code=WELCOME10" https://staging.example.com/cart/coupon &
done; wait
# Only one request should succeed.
Fix pattern: make the rule atomic in the database: after UPDATE coupons SET used = used + 1 WHERE id = ? AND used < max_uses, reject the coupon if zero rows changed, and for once-per-user codes add a unique constraint on (coupon_id, user_id). Drive multi-step flows with a server-side state machine, and limit attempts and lifetime for OTP and reset codes. Half an hour of threat modelling per important feature is a good start: who would abuse this, and how?
A07: Authentication Failures
Example: credential stuffing against wp-login.php or a forum login with leaked username and password lists; a session ID that doesn't change after login (session fixation); a session that survives logout on the server; username enumeration through different messages for "user not found" and "wrong password".Test: enter a wrong password 20 times on a test account; throttling, a temporary lock or a CAPTCHA should kick in. Compare the error messages for existing and non-existing users. Note the session cookie value before and after login; it must change. Replay the old cookie with curl after logging out; it must be rejected. Check the Secure, HttpOnly and SameSite flags on cookies in the developer tools.
Fix pattern:
INI:
; php.ini: session cookies
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1
A08: Software or Data Integrity Failures
Example: a plugin that runs PHP unserialize() on cookie data; a JavaScript file loaded from a CDN without an integrity check; an order marked "paid" by a webhook call whose signature nobody verifies; a home-made auto-updater that downloads and unpacks a zip over HTTP.Test: grep the codebase for unserialize(, pickle.loads and yaml.load( and trace where the input comes from. In WordPress, wp core verify-checksums and wp plugin verify-checksums --all compare files with WordPress.org checksums; the plugin command only covers plugins from the WordPress.org directory. Check whether external script tags in the page source carry an integrity attribute.
Fix pattern: use JSON rather than the language's native serialisation for anything that comes from a client (json_decode and json_encode in PHP). The PHP manual is explicit: never pass untrusted input to unserialize(), even with the allowed_classes option. If you must unserialise data you stored yourself, such as a cache entry or a legacy table, verify it with hash_hmac first and add ['allowed_classes' => false] as a second layer. In Python, load YAML with yaml.safe_load and never unpickle untrusted data. If you have to hand state to the client, sign it with an HMAC and verify it on the server. We covered signatures, timestamps and replay protection for webhook receivers in our secure webhook receiver guide. For CDN files, generate a Subresource Integrity (SRI) hash and add it to the script tag as integrity="sha384-..." together with crossorigin="anonymous":
Bash:
openssl dgst -sha384 -binary library.min.js | openssl base64 -A
A09: Security Logging and Alerting Failures
OWASP renamed the category to stress alerting: a log nobody looks at catches no attack.Example: a new admin account appears at 03:00 and nobody notices; the logs sit only on the compromised server's own disk, so the attacker deletes them.
Test: on staging, make 10 failed logins and create a test admin account. Were the events logged with timestamp, IP and username? Did an alert reach a person within the time you set? If the answer to the second question is no, you have an A09 finding.
Fix pattern:
- Log successful and failed logins, access-denied (403) events, server-side validation failures, role and permission changes, password and MFA changes, configuration changes and bulk data exports.
- Write logs in a machine-readable format such as JSON, and encode line breaks in user input to prevent log injection.
- Never log passwords, tokens or card numbers, and keep personal data to a minimum. Retention periods and access rights fall under data protection law too (KVKK in Türkiye, GDPR in the EU); check current obligations at the official source.
- Ship logs off the server: a central log host, Loki and Grafana, ELK or OpenSearch, or a hosted service.
- Start with three to five clear alert rules: failed logins above a set rate per minute, a new admin account, an admin login from a new country or IP, a sudden rise in 5xx errors.
A10: Mishandling of Exceptional Conditions
The new category for 2025 (24 CWEs). It covers applications that fail to prevent, detect or properly respond to unusual conditions.Example: a database error page that shows the SQL query and file paths; a catch block that lets the request through when the authorisation service times out (failing open); a money transfer interrupted after the debit is written but before the credit; an upload error path that never releases temporary files and connections.
Test: send unexpected input: missing parameters, wrong types (an array such as ?id[]=1 in PHP), broken JSON, an oversized body. On staging, stop the database or a dependent service. The response must not show a stack trace, query, version or file path; privileged actions must be denied; the event must appear in the log with a correlation ID.
Fix pattern:
INI:
; php.ini (production)
display_errors = Off
log_errors = On
expose_php = Off
PHP:
set_exception_handler(function (Throwable $e) {
$id = bin2hex(random_bytes(8));
error_log("[$id] " . $e); // details only in the server log
http_response_code(500);
echo "Something went wrong. Support code: $id";
});
try {
$allowed = $authz->canEdit($user, $post);
} catch (Throwable $e) {
error_log((string) $e);
$allowed = false; // fail closed: deny on error
}
$pdo->beginTransaction(); // PDO throws exceptions by default in PHP 8
try {
$debit->execute([$amount, $fromId]);
$credit->execute([$amount, $toId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack(); // never leave half a transaction behind
throw $e;
}
A prioritised 30-day checklist
- Days 1-3: build an inventory (domains, applications, admin panels, plugins). Update core, themes, plugins and packages; turn on MFA for admin accounts; change default passwords.
- Days 4-7: switch off debug modes, run the exposed-files test, block .env and .git, and prove your backups work by actually restoring one.
- Days 8-12: run composer audit, npm audit and pip-audit; close critical and high findings; remove abandoned and nulled components; produce your first SBOM.
- Days 13-16: scan repository history with gitleaks and rotate whatever it finds. Check the password hash format and plan rehash-on-login if needed.
- Days 17-21: run the manual IDOR check on your ten most used endpoints; ship HSTS with a short max-age and CSP in Report-Only mode; fix cookie flags.
- Days 22-25: test rate limiting on login and password-reset forms; complete the logged events and set up the first three alert rules.
- Days 26-28: test error pages and fail-closed behaviour; deploy a global exception handler.
- Days 29-30: add the CI workflow below, enable the weekly scheduled run, and book threat-modelling sessions for next quarter.
Security checks in CI
The GitHub Actions workflow below combines dependency audits, SBOM generation and a ZAP baseline scan of staging. Delete the steps for ecosystems you don't use; the same commands run in GitLab CI or Jenkins. The weekly schedule matters: new vulnerabilities in your dependencies are published even when your code doesn't change.
YAML:
name: security
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "0 5 * * 1" # every Monday 05:00 UTC
permissions:
contents: read
jobs:
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
- run: composer audit --locked --no-dev
- uses: actions/setup-node@v7
with:
node-version: "lts/*"
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=high
- uses: actions/setup-python@v7
with:
python-version: "3.x"
- run: pip install pip-audit && pip-audit -r requirements.txt
- uses: anchore/sbom-action@v0 # uploads the SBOM as a workflow artifact
with:
format: cyclonedx-json
zap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: zaproxy/[email protected]
with:
target: "https://staging.example.com"
rules_file_name: ".zap/rules.tsv"
cmd_options: "-I"
fail_action: true
allow_issue_writing: false
Kod:
10035 FAIL (Strict-Transport-Security Header)
10021 FAIL (X-Content-Type-Options Header Missing)
90022 FAIL (Application Error Disclosure)
10038 WARN (Content Security Policy (CSP) Header Not Set)
FAQ
Is our checklist based on the 2021 list obsolete?No. Most categories are still there; move SSRF to A01, widen component scanning into supply-chain control, and add A10 and alerting items.
Has SSRF left the list?
No. It is no longer a separate entry but part of A01 (CWE-918). Every feature that fetches a user-supplied URL on the server still needs testing.
Can a ZAP baseline scan harm a live site?
Baseline runs passive rules and sends no attacks, but its spider does generate requests, so watch traffic on busy sites. The full scan (zap-full-scan.py) performs real attacks; run it only against staging.
We don't write code; we run WordPress and XenForo. Does the list apply to us?
Yes. Most of your risk sits in A02 (configuration), A03 (plugins and themes), A07 (logins and MFA) and A09 (alerting). As soon as you add custom code or add-ons, A01 and A05 apply too.
Is following the OWASP Top 10 enough?
The Top 10 is an awareness document, not an audit standard. Compliance is necessary, but it is not the same thing as security; for verifiable requirements, use OWASP ASVS.
Conclusion and next steps
This week, do three things: a manual IDOR check on your most critical endpoints, a security-header check and a dependency audit. Then work through the 30-day list and make the checks permanent with the CI workflow. For the server layer, continue with our production Linux server hardening guide; if you would like an outside review, AIOR's cyber security services are one option. Which control gets skipped most often on your stack? Share it in this thread.
Son düzenleyen: