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
  1. 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

    1. Parametreyi doğrulayın: `if (!isUuid(req.params.id)) return res.status(400).json({ error: "Invalid order id." });`
    2. Birleştirilmiş sorguyu aşağıdaki yamadaki parametreli sürümle değiştirin.
    3. `src/routes` altında aynı kalıbı — `db.query("… " +` — arayın ve onları da dönüştürün.
    4. `/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.

  2. 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ü yok

    Etkisi

    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

    1. Sorguya `and user_id = $2` ekleyip `req.session.userId` değerini geçirin.
    2. Kayıt yoksa 403 yerine 404 dönün; böylece uç nokta hangi kimliklerin var olduğunu doğrulamamış olur.
    3. 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.

  3. 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

    1. Anahtarı şimdi Stripe panelinden yenileyin; commit edilmiş olan herkese açık sayılmalı.
    2. Yedek değeri yamadaki gibi sert bir hatayla değiştirin.
    3. Değeri geçmişten temizleyin (`git filter-repo`) ya da eski anahtarın açıkta kaldığını kabul edip yenilemeye güvenin.
    4. 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.

  4. 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

    1. Oturumun kullanıcısına göre bir sınırlayıcı ekleyin, örneğin saatte 5 iade.
    2. İ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.
    3. 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

  1. Commit edilmiş Stripe anahtarını döndürün.
  2. Sipariş sorgusunu parametreli hale getirin ve sahiplik kontrolünü ekleyin.
  3. İadeleri idempotent ve hız sınırlı yapın.
  4. 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
Kendi deponuzu denetleyinNasıl çalışır