Bir denetim size ne veriyor
Bu, küçük bir sipariş API'si için hazırlanmış rapor. Her bulgu dosyayı ve satırı söyler, kodu alıntılar, saldırganın ne kazandığını anlatır, yama ve doğrulama yoluyla biter.
Bu sayfa için elle yazıldı: depo kurgusaldır ve hiçbir model çağrılmadı. Rapor, tıpkı sizin denetimleriniz gibi Türkçe.
acme/checkout-api
Özet
Servis, Postgres üzerinde çalışan bir sipariş API'si ile bir Stripe webhook'u sunuyor. İki sorun, kimliği doğrulanmamış ya da yalnızca oturum açmış bir çağıran tarafından sömürülebilir durumda: sipariş sorgusunun dize birleştirmeyle kurulması ve sipariş uç noktasının URL'deki kimliği sahibini kontrol etmeden kabul etmesi. `src/config/env.ts` içinde canlı bir Stripe anahtarı commit edilmiş; bu yüzden ilk iş, geri kalanından bağımsız olarak anahtarı döndürmek. Oturum yönetimi ve webhook imza doğrulaması sağlam.
- Risk skoru
- 82
- Bulgular
- 4
01KritikSipariş sorgusunda SQL enjeksiyonuCWE-89
src/routes/orders.ts:42–46 · güven: high · fable + codex
`GET /orders/:id` sorgusunu, yol parametresi `id` değerini SQL'e birleştirerek kuruyor. Parametre doğrudan URL'den geliyor ve hiç doğrulanmıyor; yani çalışacak ifadeyi çağıran belirliyor.
const { rows } = await db.query( "select * from orders where id = " + req.params.id, );Etkisi
Müşteri adları, adresleri ve ödeme referansları dahil sipariş tablosunun tamamının okunması; veriyi yazmak ya da silmek için de makul bir yol.
İstismar senaryosu
`/orders/1 or 1=1` isteği tablodaki bütün siparişleri döndürür. `/orders/1; drop table orders --` ise buna izin veren bir sürücüde ikinci bir ifadeyi çalıştırır. Hesaba gerek yok: rota, 39. satırdaki oturum kontrolünden önce erişilebilir durumda.
Çözüm
Kimliği bağlı parametre olarak geçirin ve sorgu çalışmadan önce UUID olmayan her değeri reddedin.
Nasıl düzeltilir
- Parametreyi doğrulayın: `if (!isUuid(req.params.id)) return res.status(400).json({ error: "Invalid order id." });`
- Birleştirilmiş sorguyu aşağıdaki yamadaki parametreli sürümle değiştirin.
- `src/routes` altında aynı kalıbı — `db.query("… " +` — arayın ve onları da dönüştürün.
- `/orders/1 or 1=1` isteğinin 400 döndürdüğünü doğrulayan bir regresyon testi ekleyin.
Yama
const { rows } = await db.query( "select * from orders where id = $1", [req.params.id], );Nasıl doğrulanır
`GET /orders/1%20or%201=1` isteğini tekrarlayın: 400 dönmeli ve Postgres günlüğünde ifade, değer gömülü değil bağlı parametreli görünmeli.
02YüksekOturum açan herkes her siparişi okuyabiliyorCWE-639
src/routes/orders.ts:39–41 · güven: high
İşleyici yalnızca bir oturumun var olduğunu kontrol ediyor; siparişin sahibiyle çağıranı hiç karşılaştırmıyor. Hangi kaydın döneceğine tek başına URL'deki kimlik karar veriyor.
if (!req.session?.userId) return res.status(401).end(); // … aşağıdaki sorgudan önce sahiplik kontrolü yokEtkisi
Müşteriler arası veri sızıntısı: başka hesaplara ait adresler, tutarlar ve ödeme referansları.
İstismar senaryosu
Usulüne uygun kaydolan bir müşteri, sipariş kimliklerini sırayla deneyerek diğer müşterilerin siparişlerini okuyabilir. Günlüklerde bunu normal kullanımdan ayıran hiçbir şey yok.
Çözüm
Sorguyu oturumun kullanıcısına daraltın; böylece tek başına kimlik hiçbir zaman yeterli olmaz.
Nasıl düzeltilir
- Sorguya `and user_id = $2` ekleyip `req.session.userId` değerini geçirin.
- Kayıt yoksa 403 yerine 404 dönün; böylece uç nokta hangi kimliklerin var olduğunu doğrulamamış olur.
- Aynı daraltmayı `PATCH /orders/:id` ve `POST /orders/:id/refund` için de uygulayın.
Yama
const { rows } = await db.query( "select * from orders where id = $1 and user_id = $2", [req.params.id, req.session.userId], ); if (rows.length === 0) return res.status(404).end();Nasıl doğrulanır
Bir hesapla oturum açıp başka bir hesabın oluşturduğu siparişi isteyin: yanıt 404 olmalı ve kayıt gövdede görünmemeli.
03YüksekStripe gizli anahtarı depoya commit edilmişCWE-798
src/config/env.ts:12 · güven: high · fable + codex
Ortam değişkeni yoksa devreye giren yedek değer olarak canlı bir Stripe gizli anahtarı atanmış. Depoya — ya da geçmişine — erişebilen herkesin elinde bu anahtar var.
export const STRIPE_KEY = process.env.STRIPE_SECRET_KEY ?? "sk_live_51H…";Etkisi
Bu servisten bağımsız olarak ödeme hesabının API'sine tam erişim.
İstismar senaryosu
Anahtar her klonda ve commit geçmişinde duruyor. Döndürülene kadar canlı hesapta tahsilat, iade ve müşteri okuma yetkisi veriyor.
Çözüm
Önce anahtarı döndürün, sonra yedek değeri kaldırın ki eksik değişken açılışta yüksek sesle hata versin.
Nasıl düzeltilir
- Anahtarı şimdi Stripe panelinden yenileyin; commit edilmiş olan herkese açık sayılmalı.
- Yedek değeri yamadaki gibi sert bir hatayla değiştirin.
- Değeri geçmişten temizleyin (`git filter-repo`) ya da eski anahtarın açıkta kaldığını kabul edip yenilemeye güvenin.
- Bir sonraki anahtarın birleşmeden yakalanması için CI'ya gizli tarama ekleyin.
Yama
const key = process.env.STRIPE_SECRET_KEY; if (!key) throw new Error("STRIPE_SECRET_KEY is not set"); export const STRIPE_KEY = key;Nasıl doğrulanır
Servisi `STRIPE_SECRET_KEY` olmadan başlatın: gömülü değeri kullanmak yerine açılmayı reddetmeli.
04Ortaİade uç noktasında hız sınırı yokCWE-770
src/routes/orders.ts:88 · güven: medium
`POST /orders/:id/refund` her istekte bir Stripe çağrısı yapıyor; hesap ya da sipariş başına hiçbir sınır yok. İşleyici ayrıca idempotent değil: aynı anda gelen iki istek `status !== "refunded"` kontrolünü birlikte geçebilir.
router.post("/orders/:id/refund", requireSession, async (req, res) => {Etkisi
Çift iade ve ödeme sağlayıcısında kimsenin istemediği trafik için fatura.
İstismar senaryosu
Aynı iadeyi birkaç milisaniye arayla iki kez gönderen bir müşteri, durum geri yazılmadan siparişin iki kez iade edilmesini sağlayabilir.
Çözüm
Uç noktayı hesap başına sınırlayın ve iadeyi veritabanı düzeyinde idempotent hale getirin.
Nasıl düzeltilir
- Oturumun kullanıcısına göre bir sınırlayıcı ekleyin, örneğin saatte 5 iade.
- İadeyi koşullu bir güncellemeyle sahiplenin — `update orders set status = 'refunding' where id = $1 and status = 'paid' returning id` — ve yalnızca bir satır döndüğünde Stripe'ı çağırın.
- Stripe çağrısına idempotency anahtarı geçirin ki yeniden deneme iki kez tahsilat yapmasın.
Nasıl doğrulanır
Aynı sipariş için iki iade isteğini paralel gönderin: Stripe'a tam olarak biri ulaşmalı, ikincisi 409 dönmeli.
Sonraki adımlar
- Commit edilmiş Stripe anahtarını döndürün.
- Sipariş sorgusunu parametreli hale getirin ve sahiplik kontrolünü ekleyin.
- İadeleri idempotent ve hız sınırlı yapın.
- CI'ya gizli tarama ve sorgu kurma kalıbı için bir lint kuralı ekleyin.
İyi yapılanlar
- Stripe webhook'u imzayı, gövdeyi ayrıştırmadan önce ham istek gövdesine karşı doğruluyor.
- Oturum çerezleri üretimde `HttpOnly`, `SameSite=Lax` ve `Secure`.
- Veritabanı kimlik bilgileri ortamdan geliyor; yalnızca Stripe anahtarının commit edilmiş bir yedeği vardı.
5 dosya analiz edildi
- src/routes/orders.ts
- src/db/client.ts
- src/lib/session.ts
- src/config/env.ts
- src/routes/webhooks.ts