E-Ticarette Fiyat, Kupon ve Sepet Manipülasyonu Nasıl Önlenir?
E-ticarette fiyat, kupon ve sepet manipülasyonu çoğunlukla geçerli isteklerle, injection olmadan gerçekleşir. Güvenli bir sistem fiyatı istemciye bırakmaz; server-side pricing, kupon kuralları, idempotency ve race condition kontrolleriyle iş mantığını saldırgandan önce korur.
SECNODEX Güvenlik Ekibi
Ofansif Güvenlik
Bir ürün sayfasında 12.500 TL görünürken siparişin 125 TL üzerinden tamamlanması her zaman “kullanıcı fiyat alanını değiştirdi” kadar basit bir hatadan kaynaklanmaz. Bazen ürün fiyatı doğru alınır, fakat indirim iki kez uygulanır. Bazen tek kullanımlık kupon, eş zamanlı isteklerle onlarca siparişte tüketilir. Bazen ödeme kuruluşu doğru tutarı tahsil eder, uygulama farklı tutardaki siparişi ödendi kabul eder. Bazen de kullanıcı indirim hakkını kazandıktan sonra sepetini değiştirir ve artık sağlamadığı koşullarla checkout'u tamamlar.
Bu örneklerin ortak noktası yalnızca eksik input validation değildir. Asıl sorun, fiyatlandırma kararının hangi sistemde verildiğinin, hangi anda kesinleştiğinin ve bir sonraki adıma hangi kanıtla taşındığının belirsiz olmasıdır.
Not
E-ticarette fiyat manipülasyonu, client'tan gelen toplamı reddetmekle değil, ürün seçiminden refund'a kadar parasal bütünlüğü server-side kurallarla korumakla önlenir.
Güvenli bir mimaride browser veya mobil uygulama “bu sipariş 3.420 TL” demez. Yalnızca “şu varyanttan iki adet, şu teslimat seçeneğiyle almak ve şu kuponu denemek istiyorum” der. Fiyatı, vergi matrahını, kargoyu, kampanya uygunluğunu, kullanılacak store credit miktarını ve tahsil edilecek toplamı yetkili backend hesaplar. Ardından bu hesap bir quote veya order pricing snapshot olarak sürümlenir. Ödeme sonucu geldiğinde de yalnızca webhook imzası değil, provider'daki tutar, currency, order kimliği ve beklenen state birlikte doğrulanır.
Bu rehber, fiyat manipülasyonunu yalnızca HTTP parametresi seviyesinde değil, pricing engine, cart, promotion, payment ve refund bileşenleri arasındaki güven ilişkisi üzerinden ele alır. E-ticaret sistemlerindeki diğer kritik riskleri daha geniş bir çerçevede incelemek için e-ticaret sitelerinde en sık bulunan kritik açıklar yazısına da bakabilirsiniz.
Kısa cevap: Hangi kontroller olmadan güvenli fiyatlandırmadan söz edilemez?
Fiyat, kupon ve sepet manipülasyonunu önlemek için en az şu kontroller birlikte çalışmalıdır:
- 1Client yalnızca ürün, varyant, adet ve tercih bilgilerini göndermelidir. Birim fiyat, indirim, vergi, kargo ve toplam client'tan güvenilir veri olarak alınmamalıdır.
- 2Fiyatlandırma tek bir yetkili server-side pricing service tarafından yapılmalıdır.
- 3Ürün, varyant, price list, currency, tenant ve satış kanalı birbirine server-side bağlanmalıdır.
- 4Para değerleri floating point ile değil, minor unit veya uygun decimal türüyle işlenmelidir.
- 5Checkout öncesinde süreli ve sürümlü bir quote oluşturulmalı, order'a immutable pricing snapshot yazılmalıdır.
- 6Kupon uygunluk kontrolü ile kupon tüketimi birbirinden ayrılmalı, tüketim işlemi atomik olmalıdır.
- 7Tek kullanım, kota, bakiye ve stok gibi paylaşılan kaynaklar database constraint ve transaction ile korunmalıdır.
- 8Order, payment ve refund endpoint'leri idempotent tasarlanmalıdır.
- 9Payment sonucu yalnızca redirect veya client callback ile kesinleştirilmemelidir.
- 10Tahsil edilen amount ve currency, order'ın beklenen değeriyle karşılaştırılmadan fulfillment başlamamalıdır.
- 11Adres, kargo, ürün veya quantity değiştiğinde quote geçersiz kılınmalı ve yeniden hesaplanmalıdır.
- 12Sipariş, ödeme, kupon, gift card ve refund kayıtları düzenli reconciliation kontrollerinden geçirilmelidir.
Bu maddelerden biri eksikse sistem bazı normal testleri geçebilir, ancak concurrency, retry, eski verinin yeniden kullanılması veya servisler arası tutarsızlık karşısında yanlış fiyatla sipariş üretmeye devam edebilir.
Fiyat manipülasyonu nedir?
Fiyat manipülasyonu, kullanıcının satın alma akışındaki bir değeri veya state geçişini değiştirerek sistemin amaçlamadığı bir ekonomik sonuç üretmesidir. Sonuç her zaman doğrudan ürün fiyatının düşmesi değildir. Aşağıdaki durumların tamamı aynı risk ailesindedir:
- Birim fiyatın request içinde değiştirilmesi
- Negatif veya sıfır quantity ile toplamın azaltılması
- Daha ucuz varyantın
priceIddeğeriyle pahalı ürünün eşleştirilmesi - Kuponun kapsam dışı ürüne uygulanması
- Birbirini dışlaması gereken kampanyaların üst üste bindirilmesi
- Minimum sepet tutarı sağlandıktan sonra ürün çıkarılıp indirimin korunması
- Tek kullanımlık kuponun paralel isteklerle birden fazla kez tüketilmesi
- Süresi dolmuş fiyat teklifinin yeniden oynatılması
- TL fiyatın farklı currency koduyla ödeme servisine gönderilmesi
- Gift card veya loyalty point bakiyesinin eş zamanlı işlemlerle iki kez harcanması
- Başarısız veya düşük tutarlı payment'ın başarılı siparişe bağlanması
- Kısmi ödeme sonrasında siparişin tamamen ödenmiş sayılması
- Bir sipariş için birden fazla refund üretilmesi
- İade tutarının tahsil edilen tutarı aşması
- Marketplace siparişinde seller payının veya platform komisyonunun yanlış hesaplanması
OWASP, payment functionality testlerinde price tampering, currency değişikliği, eski fiyatın gecikmeli kullanımı, indirim sonrasında cart'ın değiştirilmesi, payment akışının atlanması ve race condition gibi senaryoları aynı business logic alanı içinde değerlendirir. Bu sınıflandırma önemlidir. Çünkü sorun tek bir endpoint'teki doğrulama eksikliğinden ziyade, ekonomik kuralların uçtan uca korunamamasıdır.
Neden yalnızca input validation yeterli değildir?
quantity alanının sayı olup olmadığını kontrol etmek gereklidir, fakat yeterli değildir. couponCode alanında yalnızca izin verilen karakterleri kabul etmek de gereklidir, fakat kuponun aynı anda kaç kez tüketilebileceğini çözmez. total değerinin pozitif olmasını istemek ise client'ın daha düşük ama pozitif bir değer göndermesini engellemez.
Input validation şu soruya cevap verir:
Not
Gelen veri beklenen biçimde mi?
Fiyatlandırma güvenliği ise daha geniş sorular sorar:
- Bu veriyi kim üretmeye yetkili?
- Bu kullanıcı bu ürünü, bu tenant ve satış kanalında satın alabilir mi?
- Bu fiyat ilgili varyanta ve currency'ye gerçekten ait mi?
- Kupon bu müşteri, ürün, tarih ve kullanım geçmişi için geçerli mi?
- Aynı işlem başka bir request tarafından tamamlandı mı?
- Bu order için ödeme beklenen tutarda mı alındı?
- State, izin verilen sırayla mı ilerledi?
- İşlem tekrarlandığında ikinci bir ekonomik etki oluşuyor mu?
CWE-602, server'ı koruması gereken mekanizmanın client tarafına bırakılmasını ayrı bir güvenlik zayıflığı olarak tanımlar. Frontend'deki disabled buton, gizli form alanı, JavaScript hesabı veya mobil uygulamadaki kontrol kullanıcı deneyimine yardımcı olabilir. Ancak saldırgan kendi HTTP isteğini oluşturabileceği için bunlar server-side kararın yerine geçemez.
Güven sınırı nerede başlar?
Browser, mobil uygulama, üçüncü taraf marketplace entegrasyonu ve hatta kurum içindeki bazı servisler güvenilmeyen veya sınırlı güvenilen veri kaynakları olarak görülmelidir. “Bizim mobil uygulamamızdan geliyor” ifadesi güvenlik kanıtı değildir. Mobile client tersine mühendislik edilebilir, proxy üzerinden değiştirilebilir veya API doğrudan çağrılabilir.
Güvenli tasarımda client'ın niyeti ile server'ın kararı ayrılır:
| Client'ın iletebileceği niyet | Server'ın belirlemesi gereken karar |
|---|---|
productId, variantId | Satılabilirlik, güncel price list ve birim fiyat |
quantity | Min, max, stok, paket büyüklüğü ve satın alma limiti |
couponCode | Uygunluk, indirim tutarı, kullanım kotası ve stacking |
shippingMethodId | Adrese uygunluk, kargo ücreti ve teslimat kısıtı |
giftCardToken | Sahiplik, bakiye, currency ve kullanılabilir tutar |
| Teslimat adresi | Vergi, bölge, risk ve kargo hesabı |
| Payment method tercihi | Tahsil edilecek amount, currency ve order eşleştirmesi |
Client'a hesaplanmış fiyatı göstermek normaldir. Tehlikeli olan, aynı fiyatı geri aldığınızda onu doğru kabul etmektir.
Güvensiz API sözleşmesi
Aşağıdaki request, ekonomik kararların neredeyse tamamını client'a bırakır:
{
"productId": "prod_742",
"variantId": "var_11",
"quantity": 2,
"unitPrice": 125,
"discountAmount": 500,
"shippingCost": 0,
"taxAmount": 1,
"grandTotal": 1,
"currency": "TRY",
"couponCode": "WELCOME"
}Backend bu alanları doğrulasa bile yanlış şeyi doğruluyor olabilir. Örneğin grandTotal > 0 kontrolü, toplamın doğru olduğunu kanıtlamaz.
Daha güvenli API sözleşmesi
Client yalnızca seçimini gönderir. Server tüm parasal alanları kendi kayıtlarından ve kurallarından üretir:
import { z } from "zod"
export const quoteRequestSchema = z.object({
items: z.array(
z.object({
productId: z.string().uuid(),
variantId: z.string().uuid(),
quantity: z.number().int().min(1).max(25)
})
).min(1).max(100),
couponCode: z.string().trim().min(3).max(40).optional(),
shippingMethodId: z.string().uuid(),
deliveryAddressId: z.string().uuid()
}).strict().strict() kullanılması, unitPrice veya grandTotal gibi sözleşme dışı alanların sessizce kabul edilmesini önler. Ancak bu da yalnızca başlangıçtır. productId ile variantId bağının, ürünün aktif olup olmadığının ve kullanıcının adres üzerinde yetkili olduğunun ayrıca doğrulanması gerekir. Nesne sahipliği kontrollerinin nasıl kaçırıldığını IDOR açığı neden hâlâ bu kadar yaygın? yazısında ayrıntılı inceledik.
Pricing attack surface nasıl çıkarılır?
Birçok ekip güvenlik incelemesini POST /checkout ile sınırlar. Oysa fiyatı etkileyen her kaynak ve fiyat kararını kullanan her consumer kapsama alınmalıdır.
Fiyatı oluşturan kaynaklar
- Product catalog ve variant kayıtları
- Price list ve müşteri segmenti fiyatları
- Kampanya ve promotion rule set
- Coupon, voucher ve referral kayıtları
- Gift card, store credit ve loyalty bakiyeleri
- Vergi ve kargo hesaplama servisleri
- Döviz kuru kaynağı
- Seller bazlı fiyat ve komisyon kayıtları
- Abonelik, bundle ve volume discount kuralları
- Bölge, kanal, tenant ve üyelik seviyesi
Fiyatı kullanan bileşenler
- Cart API
- Quote veya checkout service
- Order service
- Payment adapter
- Fulfillment ve stok rezervasyonu
- Invoice ve e-fatura entegrasyonu
- Refund ve exchange servisi
- Seller settlement
- Fraud ve accounting reconciliation
- E-posta, CRM ve müşteri hizmetleri ekranları
Bir bileşenin fiyatı doğru hesaplaması yeterli değildir. Farklı servislerin aynı sipariş için farklı gerçeğe sahip olması da manipülasyon fırsatı yaratır. Örneğin order service 3.000 TL beklerken payment adapter client'ın gönderdiği 30 TL ile PaymentIntent oluşturuyorsa doğru order hesabı ekonomik kaybı önlemez.
Fiyatlandırma zincirinin state modelini kurun
Checkout, tek bir request değil, state değişimlerinden oluşan bir süreçtir. State'ler açıkça tanımlanmadığında “ödeme başarılı sayfasına geldi, siparişi onaylayalım” gibi zayıf kestirmeler oluşur.
Örnek bir akış şöyle olabilir:
CART_MUTABLE
-> QUOTED
-> PAYMENT_PENDING
-> PAYMENT_AUTHORIZED
-> PAID
-> FULFILLMENT_STARTED
-> FULFILLED
-> PARTIALLY_REFUNDED
-> REFUNDEDBaşarısız veya alternatif geçişler de tanımlanmalıdır:
QUOTED -> QUOTE_EXPIRED
PAYMENT_PENDING -> PAYMENT_FAILED
PAYMENT_PENDING -> PAYMENT_CANCELLED
PAYMENT_AUTHORIZED -> AUTHORIZATION_VOIDED
PAID -> CANCELLATION_REQUESTEDHer geçiş için dört soruya yazılı cevap verilmelidir:
- 1Geçişi hangi actor veya servis başlatabilir?
- 2Gerekli precondition nedir?
- 3Hangi kayıtlar aynı transaction içinde değişmelidir?
- 4Aynı mesaj veya request tekrar gelirse ne olur?
Business logic açıklarının kaynak kodda bulunması da bu modelle kolaylaşır. Kod incelemesinde business logic açıkları nasıl yakalanır? yazısı, invariant ve trust boundary üzerinden yapılacak incelemeyi kod seviyesinde tamamlar.
Önce invariant'ları yazın
Invariant, sistem hangi endpoint'ten çağrılırsa çağrılsın bozulmaması gereken kuraldır. Güvenlik kontrolünü controller içine dağınık if blokları olarak eklemek yerine bu kuralları domain ve database seviyesinde korumak daha güvenlidir.
| Alan | Korunması gereken invariant |
|---|---|
| Order total | grand_total = items - discounts + shipping + tax + fees |
| Payment | Başarılı kabul edilen tahsilatın amount ve currency değeri order ile eşleşir |
| Coupon | Tüketilen toplam kullanım global ve kullanıcı limitini aşmaz |
| Gift card | Bakiye hiçbir committed state'te sıfırın altına düşmez |
| Refund | Toplam başarılı refund, toplam captured amount değerini aşmaz |
| Quantity | Pozitif tam sayıdır ve ürünün satış limitleri içindedir |
| Quote | Süresi dolmuş veya farklı cart version'a ait quote kullanılamaz |
| Variant | Fiyat kaydı aynı product, variant, tenant, channel ve currency bağlamına aittir |
| Fulfillment | Yalnızca doğrulanmış ve yeterli ödeme sonrasında başlar |
| Idempotency | Aynı mantıksal işlem en fazla bir ekonomik yan etki üretir |
Bu kurallar test edilebilir olmalıdır. “Normalde böyle oluyor” veya “UI buna izin vermiyor” bir invariant değildir.
Server-authoritative pricing nasıl uygulanır?
Pricing engine, client'tan gelen parasal sonuçları değil, ürün ve tercih kimliklerini girdi olarak almalıdır. Basitleştirilmiş bir servis arayüzü şöyle tasarlanabilir:
type Money = Readonly<{
amountMinor: bigint
currency: "TRY" | "EUR" | "USD"
}>
type PricingContext = Readonly<{
customerId: string
tenantId: string
channel: "WEB" | "MOBILE" | "MARKETPLACE"
deliveryAddressId: string
now: Date
}>
type RequestedLine = Readonly<{
productId: string
variantId: string
quantity: number
}>
type QuoteCommand = Readonly<{
items: readonly RequestedLine[]
couponCode?: string
shippingMethodId: string
}>
async function createQuote(
context: PricingContext,
command: QuoteCommand
): Promise<PricingQuote> {
const catalogLines = await catalog.resolveSellableLines({
tenantId: context.tenantId,
channel: context.channel,
items: command.items,
at: context.now
})
const base = calculateBaseAmount(catalogLines)
const promotion = await promotions.evaluate({ context, catalogLines, base })
const shipping = await shipping.calculate({ context, catalogLines, methodId: command.shippingMethodId })
const tax = await taxes.calculate({ context, catalogLines, promotion, shipping })
return quotes.persistImmutable({
context,
catalogLines,
promotion,
shipping,
tax,
rulesetVersion: promotion.rulesetVersion
})
}Bu örnekte unitPrice, discountAmount, taxAmount ve grandTotal dışarıdan alınmıyor. Ayrıca customerId, tenantId ve channel request body'sinden körlemesine kabul edilmemeli, doğrulanmış session veya service identity üzerinden üretilmelidir.
Tek pricing engine neden önemlidir?
Web uygulamasının, mobil backend'in, call center ekranının ve marketplace adapter'ının ayrı fiyat formülleri kullanması drift üretir. Bir kanal kupon stacking kuralını güncellerken diğer kanal eski davranışı sürdürebilir. Ortak bir pricing service veya ortak ve sürümlü bir domain library, ekonomik kuralların tek yerde uygulanmasına yardım eder.
Tek merkez de otomatik olarak güvenli değildir. Yüksek erişilebilirlik, cache invalidation, ruleset versioning, tenant isolation ve yetkilendirme yine çözülmelidir. Ancak “hangi bileşenin toplamı belirlemeye yetkili olduğu” netleşir.
Product, variant ve price kaydı birbirine nasıl bağlanmalı?
Sık görülen bir açıkta request hem productId hem priceId içerir. Backend iki kaydın gerçekten birbirine ait olduğunu kontrol etmeden fiyatı priceId üzerinden, ürün açıklamasını productId üzerinden alır. Kullanıcı pahalı ürünle ucuz bir fiyat kaydını birleştirebilir.
Doğrulama tek bir global primary key kontrolünden ibaret olmamalıdır. Price seçimi şu bağlamların tamamını kapsayabilir:
tenant_id
product_id
variant_id
price_list_id
sales_channel
customer_segment
currency
valid_from <= now < valid_until
active = trueEn güvenli sözleşme, client'ın mümkünse priceId göndermemesi ve price kaydını backend'in product ile bağlamdan çözmesidir. İş gereği priceId gerekiyorsa tüm bağlar tek query veya güçlü domain kontrolüyle doğrulanmalıdır.
SELECT p.id, p.amount_minor, p.currency, p.version
FROM prices p
JOIN variants v ON v.id = p.variant_id
WHERE p.id = $1
AND p.tenant_id = $2
AND p.product_id = $3
AND p.variant_id = $4
AND p.sales_channel = $5
AND p.active = TRUE
AND p.valid_from <= CURRENT_TIMESTAMP
AND (p.valid_until IS NULL OR p.valid_until > CURRENT_TIMESTAMP)Bu query sonucunun olmaması “fiyatı bulamadık, client'taki değeri kullanalım” şeklinde fallback üretmemelidir. Fiyat belirlenemiyorsa checkout güvenli biçimde durmalıdır.
Para değerlerinde floating point kullanmayın
JavaScript'te 0.1 + 0.2 sonucunun tam olarak 0.3 olmaması yalnızca estetik bir sorun değildir. İndirim, vergi, refund ve dağıtım işlemleri farklı servislerde farklı yuvarlanırsa küçük farklar kupon limitini aşmaya, eksik tahsilata veya mutabakat hatasına dönüşebilir.
Pratik seçenekler şunlardır:
- Para değerini integer minor unit olarak saklamak
- Database'de uygun precision ile
NUMERICveyaDECIMALkullanmak - Uygulama katmanında kanıtlanmış decimal kütüphanesi kullanmak
TRY için kuruş bazlı integer yaygın bir seçimdir:
type TryMoney = Readonly<{
amountMinor: bigint
currency: "TRY"
}>
function add(a: TryMoney, b: TryMoney): TryMoney {
if (a.currency !== b.currency) {
throw new Error("CURRENCY_MISMATCH")
}
return { amountMinor: a.amountMinor + b.amountMinor, currency: "TRY" }
}bigint, JSON tarafından doğrudan serialize edilmez. API sınırında minor unit değerini kurallı bir decimal string olarak taşımak veya güvenli aralıktaysa integer number kullanmak gerekir. Currency'nin ayrı ve zorunlu tutulması da önemlidir. 1000 tek başına 10,00 TL mi, 1.000 JPY mi, yoksa 10,00 USD mi olduğunu söylemez.
Yuvarlama kuralı nerede uygulanmalı?
Şunlar önceden belirlenmelidir:
- İndirim satır bazında mı, order toplamında mı hesaplanır?
- Vergi indirimden önce mi sonra mı uygulanır?
- Yüzde indirimin kesirli sonucu hangi rounding mode ile yuvarlanır?
- Son kuruş farkı satırlara nasıl dağıtılır?
- Refund sırasında orijinal dağıtım mı kullanılır, tutar yeniden mi hesaplanır?
Doğru cevap iş ve muhasebe kuralına göre değişebilir. Güvenlik açısından kritik olan, aynı kuralın pricing, payment, invoice ve refund servislerinde aynı sürümle uygulanmasıdır.
Quote, cart ve order aynı şey değildir
Cart, kullanıcı tarafından değiştirilebilir bir çalışma alanıdır. Quote, belirli girdiler ve belirli bir anda üretilmiş fiyat teklifidir. Order ise ticari işlemin kalıcı kaydıdır. Bu üç varlığı tek JSON belgesi gibi ele almak, eski fiyatın yeni cart'a taşınmasına yol açar.
Önerilen ayrım şöyledir:
cartdeğişebilir ve her değişiklikteversionartarquotebelirlicart_version, pricing ruleset ve son kullanma zamanına bağlıdırorderkabul edilen quote'un immutable snapshot'ını içerir- ödeme nesnesi order'a bağlanır, canlı cart'a değil
Örnek tablo taslağı:
CREATE TABLE pricing_quotes (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL,
cart_id uuid NOT NULL,
cart_version bigint NOT NULL,
currency char(3) NOT NULL,
subtotal_minor bigint NOT NULL CHECK (subtotal_minor >= 0),
discount_minor bigint NOT NULL CHECK (discount_minor >= 0),
shipping_minor bigint NOT NULL CHECK (shipping_minor >= 0),
tax_minor bigint NOT NULL CHECK (tax_minor >= 0),
grand_total_minor bigint NOT NULL CHECK (grand_total_minor >= 0),
ruleset_version text NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (grand_total_minor = subtotal_minor - discount_minor + shipping_minor + tax_minor)
)
CREATE TABLE orders (
id uuid PRIMARY KEY,
customer_id uuid NOT NULL,
quote_id uuid NOT NULL UNIQUE REFERENCES pricing_quotes(id),
pricing_snapshot jsonb NOT NULL,
expected_amount_minor bigint NOT NULL CHECK (expected_amount_minor >= 0),
currency char(3) NOT NULL,
state text NOT NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
)Bu şema üretim ortamı için eksiksiz değildir. Örneğin currency, order state ve tenant bağları ayrıca kısıtlanmalıdır. Buradaki önemli fikir, kabul edilen fiyatın order'a yeniden hesaplanabilir ve denetlenebilir bir snapshot olarak yazılmasıdır.
Quote ne zaman geçersiz olmalıdır?
Quote için yalnızca expires_at yeterli değildir. Aşağıdaki değişikliklerden biri gerçekleştiğinde fiyat tekrar hesaplanmalıdır:
- Ürün veya varyant değiştiğinde
- Quantity değiştiğinde
- Teslimat adresi veya vergi bölgesi değiştiğinde
- Shipping method değiştiğinde
- Coupon eklenip çıkarıldığında
- Gift card veya loyalty kullanım miktarı değiştiğinde
- Customer segment veya üyelik seviyesi değiştiğinde
- Satış kanalı ya da tenant bağlamı değiştiğinde
- Price list veya kritik promotion ruleset geçersiz kılındığında
Checkout endpoint'i şu koşulu doğrulamalıdır:
quote.cart_id == authenticated_customer.cart_id
AND quote.cart_version == current_cart.version
AND quote.customer_id == authenticated_customer.id
AND quote.expires_at > now
AND quote.status == ACTIVE
AND quote.currency == checkout_currencyTek kullanımlık quote gerekiyorsa ACTIVE -> CONSUMED geçişi order oluşturma ile aynı transaction içinde yapılmalıdır. İki request aynı quote'tan iki order üretememelidir.
Immutable pricing snapshot ne içermeli?
Snapshot yalnızca grandTotal alanından oluşmamalıdır. Sonradan “bu tutara neden ulaşıldı?” sorusunu cevaplayacak veri korunmalıdır:
- Product ve variant kimlikleri
- Görünen ürün adı ve SKU
- Her satırın quantity değeri
- Brüt ve net unit price
- Satır bazlı indirimler ve rule kimlikleri
- Order bazlı indirim dağıtımı
- Coupon kimliği ve sürümü
- Vergi oranı, matrah ve tutar
- Shipping method ve hesaplanan tutar
- Currency ve kur kaynağı gerekiyorsa rate snapshot
- Gift card, store credit ve loyalty dağılımı
- Ruleset version
- Rounding sonucu ve gerekiyorsa rounding allocation
- Oluşturulma ve sona erme zamanı
Snapshot'a hassas veri veya gereksiz kişisel veri doldurulmamalıdır. Ama yalnızca güncel product tablosuna referans vermek de yeterli değildir. Ürün fiyatı yarın değiştiğinde dünkü order'ın ekonomik gerçeği değişmemelidir.
Kupon manipülasyonu neden yalnızca code tahmini değildir?
Coupon abuse denildiğinde ilk akla gelen, kısa kupon kodlarının denenmesidir. Gerçekte daha sık ve daha ağır sorun, kuponun geçerlilik kurallarının farklı endpoint'lerde farklı uygulanmasıdır.
Bir kuponun uygunluğu en az şu boyutları içerebilir:
- Başlangıç ve bitiş zamanı
- Timezone
- Global kullanım kotası
- Müşteri başına kullanım kotası
- İlk sipariş şartı
- Minimum ve maksimum sepet tutarı
- Uygun ürün, category, seller veya brand
- Hariç tutulan ürünler
- Customer segment
- Sales channel
- Tenant veya ülke
- Currency
- Diğer kampanyalarla stacking durumu
- İndirim üst sınırı
- İade sonrasında tekrar kullanılabilirlik
- Aynı kişi için account, device, payment instrument veya household sınırı
Kupon kontrolü yalnızca is_active = true query'siyle bitmez. Kurallar tek bir evaluator içinde değerlendirilmeli ve kullanılan ruleset version snapshot'a yazılmalıdır.
Minimum sepet tutarı hangi toplam üzerinden hesaplanır?
“500 TL üzeri siparişlerde 100 TL indirim” ifadesi kod açısından belirsizdir. Şunlardan hangisi 500 TL olmalıdır?
- İndirim öncesi ürün toplamı
- Diğer promotion'lar sonrası toplam
- Vergi dahil toplam
- Kargo dahil toplam
- Gift card düşüldükten sonraki toplam
Bu belirsizlik iş birimiyle çözülmeden güvenli kod yazılamaz. Ayrıca checkout sırasında aynı tanımın kullanılması gerekir. Cart ekranında uygun görünen kuponun order oluştururken farklı formülle hesaplanması hem güvenlik hem müşteri deneyimi sorunudur.
Kupon stacking açıkları nasıl önlenir?
Promotion engine, yalnızca her kuponu ayrı ayrı geçerli bulmamalıdır. Kuponların ve otomatik kampanyaların birlikte kullanılabilirliğini de değerlendirmelidir.
Örnek bir model:
type PromotionPolicy = Readonly<{
id: string
exclusivityGroup?: string
combinableWithAutomaticPromotions: boolean
combinableCouponIds: readonly string[]
priority: number
maximumDiscountMinor?: bigint
}>
function assertStackingAllowed(applied: readonly PromotionPolicy[]): void {
const groups = new Set<string>()
for (const promotion of applied) {
if (!promotion.exclusivityGroup) continue
if (groups.has(promotion.exclusivityGroup)) {
throw new Error("PROMOTION_STACK_NOT_ALLOWED")
}
groups.add(promotion.exclusivityGroup)
}
}Gerçek sistemlerde graph benzeri uygunluk kuralları gerekebilir. Güvenlik açısından iki nokta kritiktir:
- 1Uygulama sırası deterministik olmalıdır.
- 2“En yüksek indirim” seçimi de tanımlı bir algoritmayla yapılmalıdır.
Request'teki coupon sırasının sonucu değiştirmesi veya aynı kuponun farklı büyük küçük harf varyasyonlarıyla iki kez uygulanması engellenmelidir. Kupon kodları canonical form'a dönüştürülmeli, fakat farklı Unicode karakterlerinin yanlış eşitlenmesi gibi sorunlar da hesaba katılmalıdır.
Kupon reserve, consume ve release state'leri
Kuponu cart'a yazmak, tüketmek değildir. Ödeme dakikalar sürebilir, başarısız olabilir veya kullanıcı akışı terk edebilir. Bu nedenle kampanyanın iş kuralına göre bir reservation modeli gerekebilir:
AVAILABLE -> RESERVED -> CONSUMED
\-> RELEASED
RESERVED -> EXPIREDRESERVED, kupon hakkını sınırlı süre için checkout'a ayırırCONSUMED, order'ın tanımlanan başarı noktasına ulaştığını gösterirRELEASED, payment başarısızlığı veya iptal sonrasında hakkı geri verirEXPIRED, süre bitiminde reservation'ı kapatır
Hangi noktada CONSUMED olunacağı iş kuralıdır. Order oluşturulduğunda, payment authorized olduğunda veya payment captured olduğunda tüketilebilir. Ancak karar tek olmalı, aynı event tekrar geldiğinde ikinci kez uygulanmamalıdır.
Reservation sonsuza kadar kalmamalı
Kullanıcı checkout'u terk ettiğinde kupon kotasının kilitli kalması denial of inventory benzeri bir kötüye kullanıma dönüşebilir. Reservation için kısa bir TTL, per-customer aktif reservation limiti ve güvenilir release job gerekir. Release işlemi de idempotent olmalıdır.
Race condition ile tek kullanımlık kupon nasıl çoğaltılır?
Şu kod mantıksal olarak doğru görünür:
const usage = await couponUsage.count({ couponId, customerId })
if (usage >= 1) {
throw new Error("COUPON_ALREADY_USED")
}
await couponUsage.create({ couponId, customerId, orderId })Fakat iki request aynı anda usage = 0 görebilir ve ikisi de insert yapabilir. Bu klasik check-then-act race condition'dır. CWE-362, paylaşılan kaynağa geçici ve özel erişim gerekirken başka execution flow'un aynı kaynağı değiştirebilmesini race condition olarak açıklar.
Kontrolün yalnızca application memory'de veya iki ayrı database operation arasında yapılması yeterli değildir. Database kısıtı son savunma hattı olmalıdır:
CREATE UNIQUE INDEX uq_coupon_customer_once
ON coupon_redemptions (coupon_id, customer_id)
WHERE status IN ('RESERVED', 'CONSUMED')Bu index iş kuralı gerçekten “müşteri başına bir aktif veya tüketilmiş kullanım” ise uygundur. Reservation sona erdiğinde status veya ayrı bir active flag modelinin nasıl değişeceği dikkatle tasarlanmalıdır. Partial index ve durum geçişleri gerçek iş akışıyla test edilmelidir.
Global kotayı atomik azaltmak için tek statement kullanılabilir:
UPDATE coupons
SET remaining_uses = remaining_uses - 1
WHERE id = $1
AND active = TRUE
AND remaining_uses > 0
RETURNING id, remaining_usesRETURNING satır döndürmüyorsa kullanım reddedilir. Bu işlem redemption kaydı ve order bağlantısıyla aynı transaction içinde yürütülmelidir. Aksi halde kota azalır ama order oluşmaz veya order oluşur ama kota azalmaz.
SELECT FOR UPDATE ne zaman kullanılır?
Birden fazla bağlı koşulun aynı anda değerlendirilmesi gerekiyorsa ilgili coupon veya balance satırı transaction içinde kilitlenebilir:
BEGIN;
SELECT id, remaining_uses, valid_until
FROM coupons
WHERE id = $1
FOR UPDATE;
-- Uygunluk kontrolleri ve atomik state değişiklikleri
COMMIT;Kilidi almak tek başına yeterli değildir. Tüm kritik okuma ve yazmalar aynı transaction içinde olmalı, lock ordering belirlenmeli, timeout ve deadlock davranışı test edilmelidir. PostgreSQL'in SERIALIZABLE isolation seviyesi daha güçlü bir seçenek olabilir, ancak serialization failure durumunda tüm transaction'ın güvenli biçimde yeniden denenmesi gerekir. Retry sırasında dış servise ikinci kez ödeme isteği göndermemek için idempotency ayrıca korunmalıdır.
Gift card, store credit ve loyalty point double spend
Kupon sabit bir indirim kuralı olabilir. Gift card ve store credit ise parasal bakiyeye daha yakındır. Bu nedenle bakiye kontrolü ve azaltma ayrı request'lerde yapılmamalıdır.
Güvensiz örnek:
1. Bakiyeyi oku: 1.000 TL
2. 800 TL harcanabilir mi kontrol et
3. Order oluştur
4. Bakiyeyi 200 TL olarak yazİki paralel request ilk adımda aynı bakiyeyi görürse toplam 1.600 TL harcama gerçekleşebilir.
Atomik update örneği:
UPDATE gift_cards
SET balance_minor = balance_minor - $2,
version = version + 1
WHERE id = $1
AND currency = $3
AND status = 'ACTIVE'
AND expires_at > CURRENT_TIMESTAMP
AND balance_minor >= $2
RETURNING balance_minor, versionEk olarak şunlar gerekir:
CHECK (balance_minor >= 0)database constraint- Gift card ve order arasında immutable ledger kaydı
- Her debit için unique operation kimliği
- Reversal ve refund için yeni ledger entry
- Admin adjustment işlemleri için ayrı yetki ve audit trail
- Currency dönüşümünün açık kuralı
Bakiyeyi doğrudan “eski değer + değişiklik” şeklinde güncellemek yerine append-only ledger ve türetilmiş bakiye kullanmak denetlenebilirliği artırır. Yine de ledger insert'in tekilleştirilmesi ve balance projection'ın transaction güvenliği çözülmelidir.
Quantity manipülasyonu: Negatif değerden daha fazlası
quantity >= 1 temel kontroldür. E-ticaret sistemlerinde aşağıdaki sınırlar da incelenmelidir:
- Integer olmayan quantity
NaN,Infinityve bilimsel gösterim- 32-bit veya 64-bit integer overflow
- Çok büyük quantity ile indirim veya kargo hesabının bozulması
- Bundle içindeki child quantity'nin değiştirilmesi
- Paket büyüklüğüne uymayan adet
- Dijital ürünlerde per-account limit
- Flash sale için per-customer ve global limit
- B2B ürünlerde minimum order quantity
- Ağırlık bazlı ürünlerde decimal precision
- Cart merge sonrasında limit aşımı
API sözleşmesi quantity türünü ürün tipine göre belirlemelidir. Fiziksel adet ürünü için pozitif integer beklenirken kilogram üzerinden satılan ürün için sabit precision'lı decimal gerekebilir. Her durumda toplam fiyat server-side hesaplanmalı ve limitler cart'a eklerken olduğu kadar checkout sırasında da doğrulanmalıdır.
Guest cart ile authenticated cart birleşimi
Kullanıcı giriş yaptığında anonim cart ile hesap cart'ı birleştirilir. Bu akışta sık görülen hatalar şunlardır:
- Aynı ürün quantity değerlerinin limit üstünde birleşmesi
- Anonim cart'taki kupon uygunluğunun aynen korunması
- Başka kullanıcıya ait cart token'ının hesaba bağlanması
- Farklı currency veya tenant cart'larının birleşmesi
- Eski quote'un birleşimden sonra geçerli kalması
- Gift card reservation'ın iki cart'ta birden görünmesi
Birleşim sonrasında yeni bir cart version oluşturulmalı, tüm fiyat ve uygunluk kuralları yeniden çalıştırılmalı, eski quote'lar geçersiz kılınmalıdır. Session ve cart sahipliği de server-side doğrulanmalıdır. Oturum katmanındaki riskler için oturum yönetiminde en sık yapılan hatalar rehberine bakabilirsiniz.
Adres, vergi ve kargo manipülasyonu
Kullanıcı indirimli veya ücretsiz kargo koşulunu sağladıktan sonra teslimat adresini değiştirebilir. Adres değişikliği yalnızca profil verisi olarak görülürse vergi ve shipping yeniden hesaplanmayabilir.
Şu alanlar fiyat bağlamının parçasıdır:
- Ülke, il, ilçe ve posta kodu
- Depo veya seller çıkış noktası
- Teslimat yöntemi
- Ürün ağırlığı ve hacmi
- Tehlikeli madde veya özel taşıma durumu
- Vergi bölgesi
- B2B vergi istisnası veya doğrulanmış şirket statüsü
- Ücretsiz kargo kampanyası
Kargo ücreti client'ın gönderdiği shippingCost: 0 alanından alınmamalıdır. shippingMethodId de adrese uygunluk ve fiyat açısından yeniden çözülmelidir. “Ücretsiz kargo” bir shipping method değil, belirli koşullarla elde edilen fiyat sonucu olabilir.
Stale price ve time-delayed request riski
Kullanıcı bir kampanya bitmeden quote oluşturup request'i saatler veya günler sonra yeniden gönderebilir. Frontend sayfasının açık kalması, mobile app'in offline queue kullanması veya saldırganın request'i saklaması bu durumu doğurur.
Koruma için:
- Quote'lara kısa ve iş gerekçeli expiration verin
- Price ve ruleset version saklayın
- Checkout anında quote state ve süresini doğrulayın
- Kritik kampanya iptalinde aktif quote'ları gerektiğinde revoke edin
- Server saatini güvenilir biçimde senkronize edin
- Timezone sınırlarını test edin
- Expiration kontrolünü yalnızca client countdown'a bırakmayın
İmzalı quote token kullanılıyorsa imza yalnızca verinin değiştirilmediğini kanıtlar. Süresi dolmuş ama geçerli imzalı bir token replay edilebilir. Token içinde expiration, subject, cart version, tenant, currency ve unique quote ID bulunmalı, server-side state de kontrol edilmelidir.
Cart değiştikten sonra indirim neden yeniden hesaplanmalı?
Tipik saldırı şu sırayı izler:
- 1Kullanıcı minimum sepet tutarını aşan ürünleri ekler.
- 2Kupon uygulanır ve indirim quote'a yazılır.
- 3Kullanıcı pahalı ürünü başka endpoint üzerinden çıkarır veya quantity'yi düşürür.
- 4Checkout eski indirim sonucunu kullanır.
Bu risk, kuponu yalnızca apply-coupon endpoint'inde doğrulamaktan kaynaklanır. Uygunluk her fiyatlandırma hesaplamasında veya güvenilir quote oluşturma anında yeniden değerlendirilmelidir. Cart mutation sonrası eski quote kesin biçimde geçersiz olmalıdır.
Event tabanlı mimaride invalidation mesajının gecikmesi de önemlidir. Cart service version 18'e geçmişken quote service hâlâ version 17'yi kabul etmemelidir. Checkout, yalnızca “invalidated” event'inin gelmesini beklemek yerine current cart version'ı authoritative kaynaktan doğrulamalıdır.
Payment amount server-side nasıl bağlanır?
Payment provider'a gönderilecek amount, client request'indeki toplamdan değil, order'ın expected_amount_minor ve currency alanından üretilmelidir.
Önerilen akış:
- 1Server geçerli quote'tan order oluşturur.
- 2Order'a beklenen amount ve currency snapshot yazılır.
- 3Backend provider'da payment nesnesini bu değerlerle oluşturur.
- 4Provider nesne ID'si order'a bağlanır.
- 5Client yalnızca ödeme tamamlamak için gereken sınırlı token veya secret'ı alır.
- 6Sonuç server-side webhook veya doğrulanmış provider sorgusuyla işlenir.
- 7Amount, currency, merchant account, order reference ve state eşleşmeden order
PAIDolmaz.
Stripe'ın resmi Payment Intents dokümantasyonu da PaymentIntent'ın server tarafında oluşturulmasını, tek bir shopping cart veya session ile ilişkilendirilmesini ve amount değiştiğinde güncellenmesini önerir. Bu provider'a özel bir örnek olsa da temel mimari ilkesi geneldir.
“Payment başarılı” bilgisi tek başına yeterli değildir
Webhook imzası doğru olsa bile şu kontroller yapılmalıdır:
- Event beklenen provider account'tan mı geliyor?
- Event type kabul edilen success state'i mi temsil ediyor?
- Payment object bu order'a daha önce bağlanan nesne mi?
- Provider metadata veya reference beklenen order ile eşleşiyor mu?
amount_receivedveya provider karşılığı beklenen amount'a eşit mi?- Currency birebir eşleşiyor mu?
- Payment daha önce başka order'a tahsis edildi mi?
- Event daha önce işlendi mi?
- Order'ın current state'i bu geçişe izin veriyor mu?
Signature authenticity sağlar. Business correctness sağlamaz.
Webhook güvenliğinde imza, duplicate ve sıra problemi
Payment webhook endpoint'i şu tehditleri ayrı ayrı ele almalıdır:
- 1Sahte event
- 2Gerçek event'in tekrar gönderilmesi
- 3Event'lerin farklı sırada gelmesi
- 4İşleme sırasında hata ve provider retry
- 5Aynı ödeme nesnesi için farklı event nesneleri
Provider'ın resmi library'siyle signature ve timestamp doğrulaması yapılmalıdır. Ham request body gerektiren provider'larda body parse edilmeden önce doğrulama yapılmasına dikkat edilmelidir.
Stripe, webhook event'lerinin birden fazla kez teslim edilebileceğini ve işlenen event ID'lerinin kaydedilerek duplicate işlemenin engellenmesini açıkça belirtir. Ayrıca bazı durumlarda aynı object için ayrı Event nesneleri oluşabileceğinden, object ID ile event type birleşiminin de değerlendirilmesini önerir.
Provider bağımsız pseudocode:
async function handlePaymentWebhook(rawBody: Buffer, signature: string) {
const event = provider.verifyAndParse(rawBody, signature)
return db.transaction(async tx => {
const inserted = await tx.processedEvents.insertIfAbsent({
provider: event.provider,
eventId: event.id,
objectId: event.paymentId,
eventType: event.type,
payloadHash: sha256(rawBody)
})
if (!inserted) return { status: "ALREADY_PROCESSED" }
const order = await tx.orders.findByPaymentIdForUpdate(event.paymentId)
if (!order) throw new Error("UNKNOWN_PAYMENT_OBJECT")
assertExpectedSuccessEvent(event)
assertMerchantAccount(event, order.merchantAccountId)
assertAmount(event.amountReceivedMinor, order.expectedAmountMinor)
assertCurrency(event.currency, order.currency)
assertTransitionAllowed(order.state, "PAID")
await tx.orders.markPaid(order.id, event.id)
await tx.outbox.enqueue("ORDER_PAID", { orderId: order.id })
return { status: "PROCESSED" }
})
}Outbox kullanımı, database commit ile fulfillment mesajı arasındaki dual-write sorununu azaltır. Consumer da aynı mesajı birden fazla kez alabileceği için idempotent olmalıdır.
Redirect ve success page neden siparişi onaylamamalı?
Kullanıcının /payment/success sayfasına ulaşması ödeme kanıtı değildir. URL doğrudan açılabilir, client callback değiştirilebilir veya payment tamamlanmadan redirect gerçekleşebilir.
Success sayfası yalnızca kullanıcıya durumu göstermelidir. Siparişin ekonomik state'i server-side doğrulanmış provider sonucu ile ilerlemelidir. Webhook gecikirse ekran PAYMENT_PROCESSING gösterebilir veya backend provider API'sinden kontrollü sorgu yapabilir. Ancak client'ın status=success parametresi order'ı PAID yapmamalıdır.
Idempotency neyi çözer, neyi çözmez?
Idempotency, aynı mantıksal operation tekrarlandığında ikinci bir yan etki oluşmamasını sağlar. Network timeout sonrasında client aynı request'i gönderebilir. Queue aynı mesajı yeniden teslim edebilir. Payment provider webhook'u retry edebilir. Kullanıcı ödeme butonuna iki kez basabilir.
Idempotency şu işlemlerde özellikle önemlidir:
- Quote'tan order oluşturma
- Payment object oluşturma
- Payment capture
- Coupon consume ve release
- Gift card debit ve reversal
- Fulfillment başlatma
- Refund oluşturma
- Seller settlement
Ancak bir header kabul etmek tek başına çözüm değildir. Key kapsamı, request bütünlüğü ve saklama süresi belirlenmelidir.
CREATE TABLE idempotency_records (
actor_id uuid NOT NULL,
operation text NOT NULL,
idempotency_key text NOT NULL,
request_hash text NOT NULL,
status text NOT NULL,
response_code integer,
response_body jsonb,
resource_id uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at timestamptz NOT NULL,
PRIMARY KEY (actor_id, operation, idempotency_key)
)Aynı key farklı request body ile gelirse önceki cevabı sessizce dönmek yerine conflict üretilmelidir. Bunun için canonical request hash saklanabilir. İlk işlem hâlâ sürüyorsa ikinci request'in davranışı da tanımlanmalıdır.
Idempotency key'i kullanıcı mı, server mı üretmeli?
Client retry'larını birleştirmek için client'ın stable key göndermesi yararlıdır. Fakat key'in domain resource ile server-side bağlanması gerekir. Örneğin CREATE_ORDER için key, authenticated actor ve quote ID ile ilişkilendirilebilir. Tahmin edilebilir key başka kullanıcının sonucuna erişim sağlamamalıdır. Bu nedenle scope'a actor ve operation dahil edilmelidir.
Idempotency key, authorization değildir. Aynı key'i bilen kişi order üzerinde yetki kazanmaz.
Database constraint neden application kontrolünden güçlüdür?
Application seviyesindeki kontrol, farklı pod'lar ve eş zamanlı request'ler arasında ortak gerçek olmayabilir. Database constraint ise committed state için merkezi bir sınır koyar.
Örnekler:
ALTER TABLE gift_cards
ADD CONSTRAINT ck_gift_card_non_negative
CHECK (balance_minor >= 0)
CREATE UNIQUE INDEX uq_order_quote
ON orders (quote_id)
CREATE UNIQUE INDEX uq_payment_provider_object
ON payments (provider, provider_payment_id)
CREATE UNIQUE INDEX uq_refund_provider_object
ON refunds (provider, provider_refund_id)Constraint iş kuralının tamamını tek başına ifade etmeyebilir. Örneğin dinamik tarih veya farklı tablolar arası toplam için transaction logic gerekir. Yine de mümkün olan invariant'ları database seviyesine indirmek, controller'daki unutulmuş bir kod yolunun ekonomik sınırı aşmasını önler.
Refund ve exchange akışları unutulmamalı
Fiyat güvenliği payment tamamlandığında bitmez. Refund endpoint'i, checkout'tan daha yüksek mali etki yaratabilir.
Korunması gereken başlıca kurallar:
- Refund edilecek order gerçekten captured payment'a sahip olmalıdır
- Toplam başarılı ve pending refund, captured amount'ı aşmamalıdır
- Aynı refund operation tekrar işlenmemelidir
- Satır bazlı refund quantity, satın alınan quantity'yi aşmamalıdır
- İndirim ve vergi dağılımı orijinal snapshot ile uyumlu olmalıdır
- Gift card ile ödenen tutar, politika gereği doğru araca dönmelidir
- Coupon'un iade sonrasında geri açılıp açılmayacağı deterministik olmalıdır
- Exchange fark ödemesi veya iadesi ayrı ekonomik kayıt üretmelidir
- Admin manuel refund işlemi maker-checker ve audit trail ile korunmalıdır
Atomik sınır örneği:
UPDATE payments
SET refunded_minor = refunded_minor + $2
WHERE id = $1
AND status IN ('CAPTURED', 'PARTIALLY_REFUNDED')
AND refunded_minor + $2 <= captured_minor
RETURNING captured_minor, refunded_minorProvider çağrısı ile local state arasında yine distributed transaction sorunu vardır. Sağlam bir state machine, idempotency key, provider reference ve reconciliation job gerekir. “Provider çağrısı başarılı oldu, local update başarısız oldu” senaryosu tasarım aşamasında ele alınmalıdır.
Marketplace ve multi-seller yapılarda fiyat bütünlüğü
Marketplace siparişinde tek bir toplamın altında birden fazla ekonomik taraf bulunur:
- Customer'ın ödediği tutar
- Her seller'ın ürün geliri
- Platform komisyonu
- Kargo payı
- Kampanya maliyetini üstlenen taraf
- Vergi ve kesinti
- Seller settlement
Customer total doğru olsa bile seller payı manipüle edilebilir veya yanlış kurala bağlanabilir. Seller'ın kendi fiyat kaydını güncelleyebilmesi normal olabilir, fakat başka seller'ın variant'ına fiyat yazamaması, geçmiş order snapshot'ını değiştirememesi ve settlement hesabını etkileyen alanları doğrudan belirleyememesi gerekir.
Tenant ve seller kimliği request body'den değil, doğrulanmış identity ve resource relationship üzerinden alınmalıdır. Promotion maliyeti platform ile seller arasında bölüşülüyorsa snapshot bu dağıtımı da içermelidir.
Cache ve CDN fiyatı bozabilir mi?
Evet. Cache poisoning dışında, yanlış cache key tasarımı da başka müşteri segmentinin veya currency'nin fiyatını gösterebilir ya da checkout'a taşıyabilir.
Cache key en azından gereken bağlama göre ayrılmalıdır:
tenant
sales_channel
product_or_variant
price_list
customer_segment
currency
region
ruleset_versionKişiye özel fiyatı public CDN cache'e koymak veri sızıntısı ve fiyat hatası üretebilir. Ancak gösterim katmanındaki yanlış cache hiçbir durumda checkout için authoritative olmamalıdır. Checkout, güncel ve yetkili pricing kaynağından quote üretmelidir.
Cache invalidation gecikmesi kabul edilen bir iş durumuysa quote expiration ve price protection politikası açıkça tanımlanmalıdır. Kullanıcıya gösterilen fiyat ile tahsil edilen fiyat arasında sessiz fark üretmek yerine yeniden onay alınmalıdır.
Coupon enumeration ve otomasyon nasıl sınırlandırılır?
Kupon kodu güçlü bir secret olmak zorunda değildir. Bazı kampanyalar zaten halka açıktır. Buna rağmen yüksek değerli, kişiye özel veya tek kullanımlık kodlar tahmin edilebilir olmamalıdır.
Koruma katmanları:
- Yeterli entropy'ye sahip kod üretimi
- Canonicalization sonrası uniqueness
- Hız ve hacim sınırlaması
- Account, session, IP, device ve davranış sinyallerinin birlikte değerlendirilmesi
- Başarılı ve başarısız denemeler için telemetry
- Yanıtların gereksiz ayrıntıyla kodun varlığını sızdırmaması
- Yüksek değerli kampanyalarda step-up doğrulama
- Referral ve promotion abuse için iş seviyesinde limitler
OWASP API Security Top 10 içindeki API6:2023, ekonomik veya hassas business flow'lara sınırsız otomatik erişimi ayrı bir risk olarak ele alır. Rate limiting burada önemlidir, fakat tek kontrol olmamalıdır. Dağıtık istekler, çoklu account ve düşük hızda abuse klasik IP limitlerini aşabilir.
Fraud kontrolü güvenlik kontrolünün yerine geçmez
Fraud engine, davranışsal sinyallerle şüpheli siparişleri işaretleyebilir. Fakat “fraud yakalar” diyerek yanlış amount'ı kabul etmek doğru değildir.
İki alanın görevi farklıdır:
- Pricing security, sistemin kendi ekonomik invariant'larını korur
- Fraud detection, kurallara uygun görünen ama kötü niyetli olabilecek işlemleri değerlendirir
Bir kullanıcının kendi kuponunu iki paralel request ile iki kez tüketmesi pricing integrity sorunudur. Çok sayıda sahte hesap açıp herkese açık ilk alışveriş kuponunu kullanması ise ek olarak promotion abuse ve fraud problemidir. İkisi aynı telemetry üzerinde iş birliği yapabilir, fakat mimari kontrol yerine risk skoru kullanılmamalıdır.
Logging ve reconciliation olmadan hata nasıl fark edilir?
Önleyici kontroller hata riskini azaltır. Reconciliation ise kaçan tutarsızlığı bulur. Aşağıdaki kayıtlar correlation ID ile ilişkilendirilebilmelidir:
- Cart ve cart version
- Quote ve ruleset version
- Order ve pricing snapshot hash
- Coupon reservation ve redemption
- Gift card ledger entry
- Payment provider object ve event ID
- Expected, authorized, captured ve refunded amount
- Currency
- State transition
- Actor ve service identity
- Idempotency key ve request hash
- Admin override ve gerekçesi
Günlük veya near real-time kontroller şu tutarsızlıkları arayabilir:
order.expected_amount != payment.captured_amount
order.currency != payment.currency
sum(refunds) > payment.captured_amount
coupon.redemptions > coupon.global_limit
gift_card.balance < 0
fulfilled_order without verified payment
payment without mapped order
order without immutable pricing snapshotLog'a tam kupon kodu, payment secret, kişisel veri veya card verisi yazılmamalıdır. Kupon için maskeli değer veya HMAC tabanlı aranabilir fingerprint kullanılabilir. Erişim kontrollü audit kayıtları ile operational log birbirinden ayrılmalıdır.
Alarm üretirken yalnızca başarısız isteklere bakmayın
Başarılı ama olağandışı sonuçlar daha değerlidir:
- Yüksek indirimin düşük order total'a oranı
- Aynı coupon fingerprint'in kısa sürede çok account'ta kullanılması
- Quote oluşturulduktan hemen sonra tekrarlanan cart mutation
- Aynı gift card için eş zamanlı debit girişimleri
- Aynı idempotency key ile farklı request hash
- Payment ve order amount uyuşmazlığı
- Süresi dolmak üzereyken yoğun quote tüketimi
- Aşırı refund veya refund retry
- Aynı device'tan çok sayıda ilk sipariş indirimi
Alarmın otomatik engelleme üretmesi iş etkisine göre belirlenmelidir. Ancak kritik invariant ihlallerinde sistem fail closed davranmalıdır. Örneğin payment amount order ile eşleşmiyorsa “fraud ekibi sonra bakar” denilerek fulfillment başlatılmamalıdır.
Fiyat manipülasyonu sızma testinde nasıl incelenir?
Etkili bir test yalnızca proxy'de price=1 yazmayı denemez. Önce fiyatlandırma modelini ve state'leri çıkarır, sonra kuralların farklı kombinasyonlarda korunup korunmadığını ölçer.
Keşif soruları
- Fiyatı hangi servis hesaplıyor?
- Hangi client'lar aynı API'yi kullanıyor?
- Quote var mı, ne kadar süre geçerli?
- Cart version nasıl yönetiliyor?
- Promotion engine hangi sıralamayla çalışıyor?
- Kupon ne zaman reserve ve consume ediliyor?
- Payment nesnesini kim, hangi amount ile oluşturuyor?
- Webhook hangi kontrollerden sonra order'ı ilerletiyor?
- Refund limiti nerede korunuyor?
- Concurrency ve retry senaryoları test edilmiş mi?
Negatif test aileleri
- Sözleşme dışı parasal alan ekleme
- Product, variant ve price kimliği çaprazlama
- Negative, zero, decimal ve aşırı quantity
- Currency değiştirme
- Coupon stacking sırasını değiştirme
- Minimum sepet sonrası cart mutation
- Eski quote'u yeniden oynatma
- Guest ve authenticated cart merge
- Aynı coupon veya gift card için kontrollü paralel request
- Aynı idempotency key ile farklı body
- Aynı payment event'i tekrar işleme
- Daha düşük tutarlı payment nesnesini order'a bağlama girişimi
- Partial refund ve concurrent refund
Bu testler yalnızca yazılı yetkilendirme ve tanımlı Rules of Engagement kapsamında yapılmalıdır. Üretimde paralel request, gerçek kupon tüketimi, stok rezervasyonu veya payment işlemi ekonomik ve operasyonel etki oluşturabilir. Test account, sandbox provider, sentetik ürün ve geri alınabilir veri kullanımı baştan planlanmalıdır.
Web, API ve mobil client'ların aynı backend'i kullandığı varsayımı da doğrulanmalıdır. Farklı test kapsamlarının neden gerekebildiğini API ve mobil uygulama için ayrı sızma testi gerekir mi? yazısında açıklıyoruz.
Kaynak kod analizinde nerelere bakılır?
Business logic zafiyetleri çoğu zaman tek bir scanner kuralıyla bulunamaz. Uzman incelemesi, veri akışını ve ekonomik state geçişlerini takip eder.
Öncelikli code search alanları:
price,amount,total,subtotal,discount,coupongiftCard,credit,points,balancequote,cart,checkout,orderpayment,capture,refund,webhookidempotency,retry,lock,transaction- State değişimleri ve status enum'ları
- Admin override endpoint'leri
- Event consumer ve scheduled job'lar
İnceleme şu sorularla derinleştirilir:
- 1Client-controlled amount hangi sink'e kadar gidiyor?
- 2Aynı fiyat formülü kaç yerde kopyalanmış?
- 3Transaction sınırı hangi repository çağrılarını kapsıyor?
- 4Dış servis çağrısı transaction'ın neresinde?
- 5Unique constraint var mı, hata doğru işleniyor mu?
- 6Retry aynı ekonomik etkiyi yeniden üretebilir mi?
- 7State transition merkezi mi, doğrudan status update yapılabiliyor mu?
- 8Background job eski snapshot ile işlem yapıyor mu?
- 9Webhook signature sonrasında amount ve currency doğrulanıyor mu?
- 10Refund ve reversal aynı invariant'ları koruyor mu?
Kaynak kod analizi kapsamlı rehberi, SAST ile manuel secure code review'un birlikte nasıl konumlandırılması gerektiğini daha geniş biçimde ele alır.
Güvenli bir checkout service için örnek akış
Aşağıdaki pseudocode, kontrollerin sırasını gösterir. Framework'e ve iş kurallarına göre uyarlanmalıdır:
async function createOrderFromQuote(input: {
actorId: string
quoteId: string
idempotencyKey: string
}) {
return database.serializableTransaction(async tx => {
const requestHash = sha256Canonical({ quoteId: input.quoteId })
const replay = await tx.idempotency.beginOrReplay({
actorId: input.actorId,
operation: "CREATE_ORDER",
key: input.idempotencyKey,
requestHash
})
if (replay.completed) return replay.response
const quote = await tx.quotes.findForUpdate(input.quoteId)
if (!quote) throw new DomainError("QUOTE_NOT_FOUND")
if (quote.customerId !== input.actorId) throw new DomainError("QUOTE_FORBIDDEN")
if (quote.status !== "ACTIVE") throw new DomainError("QUOTE_NOT_ACTIVE")
if (quote.expiresAt <= new Date()) throw new DomainError("QUOTE_EXPIRED")
const cart = await tx.carts.findCurrentVersion(quote.cartId)
if (cart.version !== quote.cartVersion) throw new DomainError("CART_CHANGED")
await tx.coupons.consumeReservationsAtomically(quote.couponReservations)
await tx.credits.commitReservationsAtomically(quote.creditReservations)
const order = await tx.orders.insertFromSnapshot({
customerId: input.actorId,
quoteId: quote.id,
pricingSnapshot: quote.snapshot,
expectedAmountMinor: quote.grandTotalMinor,
currency: quote.currency,
state: "PAYMENT_PENDING"
})
await tx.quotes.markConsumed(quote.id, order.id)
await tx.outbox.enqueue("PAYMENT_REQUESTED", { orderId: order.id })
const response = { orderId: order.id, state: order.state }
await tx.idempotency.complete(replay.recordId, response)
return response
})
}Burada payment provider çağrısı doğrudan açık database transaction içinde yapılmıyor. Outbox consumer order snapshot'tan payment nesnesi oluşturabilir ve provider'a ayrı idempotency key gönderebilir. Bu tercih her mimariye uymayabilir, fakat lock süresini dış ağ çağrısına bağlamama ve retry davranışını kontrol etme avantajı sağlar.
Concurrency testi nasıl yazılır?
Unit test, iki request'in aynı anda çalıştığı koşulu tek başına kanıtlamaz. Integration test gerçek database constraint ve transaction davranışını kullanmalıdır.
Basitleştirilmiş Vitest örneği:
import { describe, expect, it } from "vitest"
describe("single-use coupon", () => {
it("allows only one committed redemption under concurrency", async () => {
const coupon = await fixtures.singleUseCoupon()
const customer = await fixtures.customer()
const quotes = await fixtures.twoEligibleQuotes({ coupon, customer })
const results = await Promise.allSettled(
quotes.map((quote, index) =>
checkout.createOrder({
actorId: customer.id,
quoteId: quote.id,
idempotencyKey: `concurrency-${index}`
})
)
)
const fulfilled = results.filter(result => result.status === "fulfilled")
const rejected = results.filter(result => result.status === "rejected")
const redemptions = await db.couponRedemptions.countCommitted({
couponId: coupon.id,
customerId: customer.id
})
expect(fulfilled).toHaveLength(1)
expect(rejected).toHaveLength(1)
expect(redemptions).toBe(1)
})
})Testin güvenilir olması için iki transaction'ın kritik noktada gerçekten çakışması sağlanabilir. Test-only barrier veya database advisory mechanism kullanılabilir. Rastlantısal timing'e dayanan testler bazen açık olmasına rağmen geçer.
Hangi concurrency senaryoları test edilmeli?
- Aynı quote'tan iki order
- Tek kullanımlık coupon için iki checkout
- Global son kupon hakkı için farklı müşteriler
- Aynı gift card bakiyesi için iki order
- Aynı stoktaki son ürün için iki order
- Aynı payment için iki capture
- Aynı order için iki refund
- Coupon release ile consume yarışması
- Reservation expiration job ile checkout yarışması
- Webhook ile manuel status sorgusunun aynı anda gelmesi
Her test yalnızca “kaç response 200 döndü?” sorusuna bakmamalıdır. Database'in final state'i, provider mock çağrı sayısı ve ledger kayıtları da doğrulanmalıdır.
Property-based test ile pricing invariant'ları
Promotion kombinasyonlarının sayısı büyüdükçe yalnızca örnek bazlı testler boşluk bırakır. Property-based test, çok sayıda geçerli ve sınır girdisinde invariant'ı doğrulayabilir.
import fc from "fast-check"
import { expect, it } from "vitest"
it("never produces a negative payable total", () => {
fc.assert(
fc.property(
fc.array(
fc.record({
unitPriceMinor: fc.bigInt({ min: 0n, max: 10_000_000n }),
quantity: fc.integer({ min: 1, max: 25 })
}),
{ minLength: 1, maxLength: 30 }
),
fc.bigInt({ min: 0n, max: 100_000_000n }),
(lines, requestedDiscountMinor) => {
const quote = pricing.calculateForTest(lines, requestedDiscountMinor)
expect(quote.grandTotalMinor).toBeGreaterThanOrEqual(0n)
expect(quote.discountMinor).toBeLessThanOrEqual(quote.discountableBaseMinor)
}
)
)
})Bu test business rule'un yerini tutmaz. Yanlış tanımlanmış invariant'ı başarıyla doğrulayabilir. Bu nedenle finance, product, engineering ve security ekipleri önce beklenen ekonomik kuralları netleştirmelidir.
Fiyatlandırma için regression test matrisi
| Test boyutu | Örnek varyasyonlar |
|---|---|
| Quantity | 0, 1, max, max+1, decimal, çok büyük değer |
| Currency | TRY, desteklenmeyen kod, order ile farklı currency |
| Quote | Aktif, süresi dolmuş, consumed, eski cart version |
| Coupon | Geçerli, süresi dolmuş, kota dolmuş, farklı müşteri, farklı tenant |
| Stacking | Otomatik kampanya + coupon, iki exclusive coupon |
| Payment | Eksik tutar, fazla tutar, farklı currency, duplicate event |
| Retry | Aynı key aynı body, aynı key farklı body, yeni key aynı quote |
| Concurrency | Aynı coupon, gift card, quote, refund ve payment |
| Refund | Tam, kısmi, üst limit, eş zamanlı, daha önce işlenmiş |
| Cart mutation | Ürün, quantity, adres, shipping ve coupon değişimi |
Bu matris release öncesi otomatik testlere, dönemsel sızma testi kapsamına ve kaynak kod incelemesine ayrı ayrı yansıtılmalıdır.
Sık yapılan savunma hataları
1. Fiyat alanını request'ten kaldırınca sorunun çözüldüğünü düşünmek
Client toplam göndermese bile ucuz priceId, yanlış variant, eski quote veya farklı currency kullanabilir. Bağlam bütünlüğü ayrıca doğrulanmalıdır.
2. Frontend'de butonu kapatmak
Disabled buton ve JavaScript kontrolü API'yi korumaz. Server tüm kuralları yeniden uygulamalıdır.
3. Yalnızca bir endpoint'i düzeltmek
Web checkout güvenli olabilir, fakat mobile v1 endpoint'i veya call center servisi eski fiyatı kabul edebilir. Tüm entry point'ler aynı domain kuralını kullanmalıdır.
4. SELECT ardından INSERT ile tek kullanımı korumak
Concurrency altında iki request aynı sonucu görebilir. Unique constraint ve transaction gerekir.
5. Her şeyi SERIALIZABLE yapıp retry'ı unutmamak
Serializable güçlüdür, fakat serialization failure normal bir olasılıktır. Tüm transaction yeniden denenirken dış yan etkilerin çoğalmaması gerekir.
6. Webhook imzasını yeterli görmek
İmza event'in kaynağını doğrular. Amount, currency, order reference ve state uygunluğunu doğrulamaz.
7. Kuponu payment başlamadan kalıcı tüketmek
Başarısız payment'lar hakkı haksız yere tüketebilir. Reserve, consume ve release politikası yazılmalıdır.
8. Idempotency key'i global kullanmak
Key actor ve operation ile scope edilmezse veri sızıntısı veya yanlış replay oluşabilir.
9. Fiyatı yeniden hesaplayarak geçmiş order'ı yorumlamak
Product ve promotion kayıtları değişir. Geçmiş işlem immutable snapshot üzerinden değerlendirilmelidir.
10. Yalnızca scanner sonucuna güvenmek
Scanner parametreleri değiştirebilir, ancak kuponun iş kuralını, doğru stacking sırasını veya refund invariant'ını çoğu zaman bilemez. Manuel sızma testi ve secure code review gerekir.
Güvenli geliştirme kontrol listesi
API ve trust boundary
- [ ] Client'tan unit price, discount, tax, shipping veya grand total güvenilir veri olarak alınmıyor
- [ ] Request sözleşmesi bilinmeyen parasal alanları reddediyor
- [ ] Customer, tenant ve sales channel doğrulanmış identity'den çözülüyor
- [ ] Product ile variant ilişkisi server-side doğrulanıyor
- [ ] Price kaydı product, variant, tenant, channel ve currency ile bağlanıyor
- [ ] Fiyat bulunamadığında client değeriyle fallback yapılmıyor
- [ ] Eski API sürümleri aynı güvenlik kurallarını uyguluyor
- [ ] Admin ve call center akışları ayrı authorization ve audit kontrollerine sahip
Para ve hesaplama
- [ ] Floating point yerine minor unit veya uygun decimal türü kullanılıyor
- [ ] Currency her parasal değerle birlikte taşınıyor
- [ ] Currency dönüşümü açık kaynak, zaman ve rounding kuralına sahip
- [ ] Discount toplamı discountable base'i aşamıyor
- [ ] Grand total negatif olamıyor
- [ ] Vergi ve rounding kuralları tüm servislerde tutarlı
- [ ] Refund hesabı orijinal pricing snapshot'a dayanıyor
- [ ] Pricing ruleset version kaydediliyor
Cart, quote ve order
- [ ] Cart her mutation'da version artırıyor
- [ ] Quote belirli cart version'a bağlı
- [ ] Quote expiration server-side doğrulanıyor
- [ ] Ürün, quantity, adres, shipping veya coupon değişince quote geçersiz oluyor
- [ ] Quote customer ve tenant'a bağlı
- [ ] Tek quote'tan en fazla bir order üretilebiliyor
- [ ] Order immutable pricing snapshot saklıyor
- [ ] Payment canlı cart'a değil order'a bağlanıyor
Coupon ve promotion
- [ ] Coupon başlangıç ve bitiş zamanı doğru timezone ile kontrol ediliyor
- [ ] Global ve customer kullanım limitleri transaction güvenli
- [ ] Tek kullanım database unique constraint ile destekleniyor
- [ ] Minimum sepet tanımı açık ve tek yerde uygulanıyor
- [ ] Uygun product, category, seller ve hariç listeleri doğrulanıyor
- [ ] Coupon stacking deterministik kurallara sahip
- [ ] Coupon code canonicalization ve uniqueness politikası tanımlı
- [ ] Reserve, consume, release ve expiration state'leri açık
- [ ] Terk edilen checkout reservation'ları güvenli biçimde serbest bırakılıyor
- [ ] Coupon enumeration ve otomasyon izleniyor
Gift card ve loyalty
- [ ] Bakiye azaltma atomik statement veya uygun row lock kullanıyor
- [ ] Bakiye için database
CHECKconstraint bulunuyor - [ ] Debit ve reversal append-only ledger ile izleniyor
- [ ] Her operation unique reference taşıyor
- [ ] Gift card sahiplik, status, expiration ve currency kontrolü yapılıyor
- [ ] Paralel harcama integration test ile doğrulanıyor
Payment ve webhook
- [ ] Provider payment amount server-side order snapshot'tan oluşturuluyor
- [ ] Provider payment ID order'a tekil bağlanıyor
- [ ] Webhook signature resmi library ile doğrulanıyor
- [ ] Ham body gereksinimi doğru uygulanıyor
- [ ] Event ID duplicate kayıtları engelliyor
- [ ] Object ID ve event type düzeyinde duplicate senaryosu ele alınıyor
- [ ] Amount ve currency order ile karşılaştırılıyor
- [ ] Merchant account ve order reference doğrulanıyor
- [ ] Success page order state'ini doğrudan değiştirmiyor
- [ ] Fulfillment yalnızca doğrulanmış yeterli payment sonrasında başlıyor
Idempotency ve concurrency
- [ ] Order, payment, coupon, credit ve refund işlemleri idempotent
- [ ] Idempotency key actor ve operation ile scope ediliyor
- [ ] Aynı key farklı request body ile geldiğinde conflict üretiliyor
- [ ] Serialization failure için kontrollü retry var
- [ ] Retry dış servis yan etkisini çoğaltmıyor
- [ ] Unique constraint ihlalleri domain hatasına çevriliyor
- [ ] Lock sırası ve timeout davranışı belirlenmiş
- [ ] Kritik yarış koşulları gerçek database ile test ediliyor
Refund, izleme ve operasyon
- [ ] Refund toplamı captured amount'ı atomik olarak aşamıyor
- [ ] Line refund quantity satın alınan miktarı aşamıyor
- [ ] Refund provider ID tekil
- [ ] Admin override maker-checker ve audit trail ile korunuyor
- [ ] Expected, authorized, captured ve refunded tutarlar reconcile ediliyor
- [ ] Fulfilled but unpaid order alarmı bulunuyor
- [ ] Coupon kotası ve gift card bakiyesi için tutarsızlık kontrolleri var
- [ ] Log'larda secret, tam kupon kodu ve gereksiz kişisel veri bulunmuyor
- [ ] Incident runbook ekonomik işlemi güvenli biçimde durdurmayı kapsıyor
Bir bulgu nasıl raporlanmalı?
“Fiyat değiştirilebiliyor” ifadesi geliştirme ekibine yeterli bağlam vermez. İyi bir bulgu en az şu alanları içermelidir:
- Etkilenen endpoint ve actor
- Gerekli precondition
- Manipüle edilen state veya parametre
- Normal ve anormal request akışı
- Concurrency gerekiyorsa istek sayısı ve senkronizasyon şekli
- Beklenen ve gerçekleşen ekonomik sonuç
- Etkilenen invariant
- Tek sipariş ve ölçekli abuse için iş etkisi
- Database ve servis katmanındaki root cause
- Güvenli remediation yaklaşımı
- Regression test önerisi
- Retest sonucu
Örnek başlık:
Not
Tek kullanımlık kuponun atomik olmayan doğrulama nedeniyle eş zamanlı siparişlerde birden fazla kez tüketilebilmesi
Örnek root cause:
Not
Uygulama, kullanım sayısını transaction dışında okuyor ve ardından ayrı bir insert yapıyor. (coupon_id, customer_id) için database uniqueness kısıtı bulunmadığından iki paralel request aynı uygunluk sonucuyla commit olabiliyor.
Öneri yalnızca “rate limit ekleyin” olmamalıdır. Rate limit abuse maliyetini yükseltebilir, ancak yarış koşulunu ortadan kaldırmaz. Atomik state değişimi, database constraint, idempotency ve concurrency regression testi birlikte önerilmelidir.
Risk seviyesi nasıl belirlenir?
Fiyat manipülasyonu her zaman aynı severity'ye sahip değildir. Değerlendirme şu faktörleri kapsamalıdır:
- Authentication gereksinimi
- Kullanıcı başına veya global etki
- Otomasyona uygunluk
- Finansal kaybın üst sınırı
- Dijital ürünün anında teslim edilmesi
- Fiziksel siparişin fulfillment öncesi durdurulabilirliği
- Etkinin fark edilme süresi
- Muhasebe ve vergi kaydı etkisi
- Chargeback ve operasyon maliyeti
- Marketplace seller veya müşteri etkisi
- Çoklu account ile limit aşılabilmesi
- Tekrar kullanılabilirlik
CVSS teknik şiddeti anlatmaya yardım eder, fakat ekonomik kaybı tek başına modellemez. Raporda somut iş senaryosu ayrıca yazılmalıdır. “Bir request ile 10 TL indirim” ile “sınırsız dijital ürün alımı” aynı başlık altında olsa da aynı önceliğe sahip değildir.
SECNODEX yaklaşımı: Scanner'dan önce iş kuralını anlamak
Fiyat, kupon ve sepet manipülasyonu scanner çıktısı üzerinden sağlıklı değerlendirilemez. Test ekibinin ürünün nasıl para kazandığını, hangi kampanyaların birlikte kullanılabildiğini, ödeme ile fulfillment arasındaki sınırı ve iade politikasını anlaması gerekir.
SECNODEX, e-ticaret uygulamalarında web arayüzünü, API akışlarını ve gerektiğinde ilgili source code'u birlikte değerlendirir. Çalışma yalnızca request parametresi değiştirerek bitmez. Quote yaşam döngüsü, promotion kuralları, transaction sınırları, idempotency, webhook işleme, gift card ve refund akışları manuel olarak incelenir. Bulgular teknik PoC, iş etkisi, root cause ve uygulanabilir düzeltme adımlarıyla raporlanır.
OSCP ve OSWE sertifikalarına sahip uzmanların yer aldığı ekibimiz, otomatik taramanın gösteremediği business logic ve code-level ilişkilere odaklanır. Kurum sertifikasıyla uzman sertifikasını birbirine karıştırmadan, çalışmanın kalitesini kapsam, yöntem, kanıt ve retest sonucu üzerinden ortaya koyarız.
E-ticaret uygulamanız için saldırı yüzeyini netleştirmek üzere Sızma Testi hizmetimizi, transaction ve pricing kodunu derinlemesine değerlendirmek üzere Kaynak Kod Analizi hizmetimizi inceleyebilirsiniz.
Sonuç
E-ticarette fiyat manipülasyonu, tek bir price parametresinin kontrol edilmemesinden daha geniş bir sorundur. Ürün fiyatı doğru hesaplanırken kupon iki kez tüketilebilir. Kupon doğru uygulanırken eski quote yeniden kullanılabilir. Quote güvenli olabilir, fakat payment daha düşük tutarla order'a bağlanabilir. Payment doğru alınırken refund limiti aşılabilir.
Bu nedenle güvenli tasarımın birimi endpoint değil, ekonomik işlemin tamamıdır. Client yalnızca niyet bildirir. Server fiyatı yetkili kaynaktan üretir. Quote süreli ve sürümlüdür. Order immutable snapshot taşır. Kupon, bakiye ve refund sınırları atomik işlemlerle korunur. Payment amount ve currency doğrulanır. Retry idempotent işlenir. Reconciliation ise teoride korunması gereken kurallarla gerçek kayıtların uyuşup uyuşmadığını sürekli kontrol eder.
Uygulamanızda fiyat, kupon, gift card, payment veya refund akışlarının bu kuralları gerçekten koruyup korumadığını görmek için SECNODEX ile kapsam görüşmesi planlayabilirsiniz. Kapsamı, üretim güvenliğini ve test hesaplarını birlikte belirleyerek yalnızca semptomu değil, ekonomik kaybı mümkün kılan root cause'u doğrularız.
Teknik kaynaklar
- OWASP Web Security Testing Guide, Test Payment Functionality
- OWASP Web Security Testing Guide, Test Number of Times a Function Can Be Used Limits
- OWASP API Security Top 10 2023, API6 Unrestricted Access to Sensitive Business Flows
- OWASP Web Parameter Tampering
- CWE-602, Client-Side Enforcement of Server-Side Security
- CWE-362, Race Condition
- PostgreSQL Documentation, Transaction Isolation
- Stripe Documentation, Payment Intents API
- Stripe Documentation, Webhook Best Practices
Sık sorulan sorular
Fiyat manipülasyonu nedir?
Fiyat manipülasyonu, satın alma akışındaki veri veya state değişikliklerinden yararlanarak sistemin amaçlamadığı bir tutarla sipariş, indirim, payment, refund ya da bakiye hareketi üretmektir. Doğrudan unit price değiştirme bunun yalnızca bir örneğidir.
Fiyatı backend'de hesaplamak tek başına yeterli mi?
Hayır. Backend hesabına ek olarak product ve variant bağları, quote version, coupon uygunluğu, payment amount eşleşmesi, transaction güvenliği, idempotency ve refund sınırları korunmalıdır.
Client'a fiyat göndermek güvensiz mi?
Hayır. Fiyatın kullanıcıya gösterilmesi normaldir. Güvensiz olan, client'ın geri gönderdiği fiyatı yetkili karar olarak kabul etmektir. Server kendi güvenilir kaynaklarından yeniden hesaplamalı veya geçerli quote snapshot'ını kullanmalıdır.
Kupon neden iki paralel request ile birden fazla kez kullanılabilir?
Uygulama önce kullanım sayısını okuyup sonra ayrı işlemle kayıt oluşturuyorsa iki request aynı eski değeri görebilir. Atomik update, transaction ve database unique constraint ile bu yarış koşulu önlenmelidir.
Rate limiting kupon abuse'u çözer mi?
Tek başına çözmez. Enumeration ve otomasyon hızını azaltabilir, fakat tek kullanımlık kuponun atomik olmayan tüketimini düzeltmez. Rate limiting, domain invariant ve database kontrollerini tamamlayan bir katmandır.
Quote ile order arasındaki fark nedir?
Quote, belirli cart version ve kurallara göre sınırlı süre için üretilen fiyat teklifidir. Order ise kabul edilen teklifin kalıcı ticari kaydıdır. Order'ın immutable pricing snapshot saklaması gerekir.
İmzalı cart veya quote token yeterli midir?
İmza verinin değiştirilmediğini gösterebilir. Token'ın süresinin dolmadığını, doğru kullanıcıya ait olduğunu, replay edilmediğini veya cart'ın değişmediğini tek başına kanıtlamaz. Server-side state doğrulaması gerekir.
Webhook imzası doğrulanıyorsa payment güvenli midir?
Webhook'un gerçek kaynaktan geldiği doğrulanmış olur. Yine de payment object, amount, currency, merchant account, order reference, duplicate event ve izin verilen state transition kontrol edilmelidir.
Para hesabında neden floating point kullanılmamalı?
Binary floating point bazı decimal değerleri tam temsil edemez. İndirim, vergi ve refund hesaplarında farklı sonuçlar oluşabilir. Minor unit integer veya uygun decimal türü ve açık rounding kuralı tercih edilmelidir.
Idempotency ile unique constraint aynı şey mi?
Hayır. Idempotency aynı mantıksal operation'ın retry edilmesini yönetir. Unique constraint ise belirli database state'lerinin çoğalmasını engeller. Kritik işlemlerde çoğu zaman ikisi birlikte gerekir.
Fiyat manipülasyonu SAST ile bulunabilir mi?
Bazı client-controlled data flow'ları, eksik transaction kullanımı veya riskli sayı türleri SAST tarafından işaretlenebilir. Ancak kampanya kuralının doğruluğu ve state sırası çoğu zaman business context gerektirir. Otomatik SAST, manuel secure code review ve dinamik test birlikte daha güçlü sonuç verir.
Üretimde fiyat manipülasyonu testi yapılabilir mi?
Yazılı yetkilendirme, sentetik hesap ve ürünler, harcama sınırı, provider sandbox veya kontrollü payment yöntemi, geri alma planı ve açık Rules of Engagement varsa sınırlı testler yapılabilir. Concurrency, gerçek kupon ve refund testlerinin operasyonel etkisi ayrıca yönetilmelidir.
Ne sıklıkla test edilmelidir?
Yeni pricing engine, promotion özelliği, payment provider, mobile API, marketplace entegrasyonu veya refund akışı devreye girdiğinde test yapılmalıdır. Buna ek olarak risk ve değişim hızına göre dönemsel sızma testi, sürekli regression testi ve kritik kod değişikliklerinde secure code review uygulanmalıdır.
Fiyat manipülasyonu yalnızca e-ticaret sitelerinde mi görülür?
Hayır. Biletleme, yemek siparişi, seyahat, abonelik, oyun içi satın alma, fintech, marketplace ve dijital içerik platformlarında aynı risk ailesi görülür. Ürün adı değişse de amount, quota, credit ve state transition invariant'ları benzerdir.
Okumaya devam et
Sızma Testi
E-Ticaret Sitelerinde En Sık Bulunan Kritik Zafiyetler: E-Ticaret Sızma Testi Rehberi
E-ticaret güvenliği yalnızca SQL injection veya XSS aramakla ölçülemez. Fiyatın hangi sistemde belirlendiği, ödeme sonucuna neden güvenildiği, kupon ve refund haklarının eşzamanlı request’lerde nasıl korunduğu çoğu zaman gerçek kritik riski belirler.
Yazıyı okuKaynak Kod Analizi
Kod İncelemesinde İş Mantığı (Business Logic) Açıkları Nasıl Yakalanır?
Business logic açıkları çoğu zaman geçerli request’ler, izin verilen endpoint’ler ve syntactically doğru veriler kullanılarak ortaya çıkar. Bu nedenle etkili kod incelemesi yalnızca tehlikeli function aramaz, uygulamanın değişmez iş kurallarını, state transition’larını ve transaction sınırlarını kanıt üzerinden değerlendirir.
Yazıyı oku