Skip to main content
Kaynak Kod Analizi · 44 dk okuma

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.

SX

SECNODEX Güvenlik Ekibi

Ofansif Güvenlik

Bir e-ticaret uygulamasının checkout kodu inceleniyor. Input validation yerinde. SQL query’ler parameterized. Kart bilgisi uygulamadan geçmiyor. Kullanıcı authenticated ve endpoint üzerinde authorization kontrolü var. SAST raporunda kritik finding bulunmuyor.

Kod ilk bakışta temiz görünüyor.

Fakat CheckoutRequest içinde unitPrice alanı bulunuyor. Frontend sepette gördüğü fiyatı backend’e gönderiyor, backend de bu değeri quantity ile çarparak ödeme tutarını oluşturuyor. Negatif fiyat kabul edilmiyor, para alanı BigDecimal olarak işleniyor ve bütün request schema validation’dan geçiyor.

Saldırganın injection yapmasına gerek yok. unitPrice değerini 1499.00 yerine 14.99 göndermesi yeterli.

Buradaki sorun veri tipinin yanlış olması değildir. Request de teknik açıdan geçerlidir. Sorun, fiyatı belirleme yetkisinin müşteriye ait olmamasına rağmen backend’in istemciyi authoritative source olarak kabul etmesidir.

Başka bir uygulamada fiyat server-side hesaplanıyor olabilir. Buna karşılık aynı indirim kodu eşzamanlı iki request ile iki kez kullanılabilir. Bir bankacılık akışında transferi oluşturan kişinin aynı transferi onaylamaması gerekirken her iki endpoint de yalnızca MANAGER rolünü kontrol ediyor olabilir. Bir rezervasyon sisteminde “ödendi” adımına ulaşmak için ödeme provider callback’i beklenmesi gerekirken kullanıcı confirm endpoint’ini doğrudan çağırabiliyor olabilir.

Bu örneklerin ortak noktası şudur:

Not

Kod çalışır, request geçerlidir ve kullanılan function tek başına tehlikeli değildir. Güvenlik açığı, uygulamanın gerçekleştirmemesi gereken bir iş sonucunu gerçekleştirmesinden doğar.

Business logic açıklarını yakalamanın yolu daha fazla dangerous function listesi çıkarmak değildir. Önce uygulamanın hangi sonuçlara yalnızca hangi koşullarda izin vermesi gerektiğini tanımlamak, ardından kodun bu koşulları bütün entry point’lerde, bütün state’lerde ve eşzamanlı execution altında gerçekten enforce edip etmediğini kanıtlamaktır.

Bu yazıda kod incelemesinde iş mantığı açıklarının nasıl araştırıldığını, business invariant’ların nasıl çıkarıldığını, authorization ile workflow validation’ın nerede ayrıldığını, race condition ve idempotency problemlerinin nasıl yakalandığını ve Java Spring kodunda hangi pattern’lerin özellikle incelenmesi gerektiğini adım adım ele alacağız.

1. Business logic açığı nedir?

Business logic açığı, uygulamanın teknik olarak geçerli bir işlem dizisini, business rule’lara aykırı bir sonuca ulaştırabilmesidir. Saldırgan çoğu zaman uygulamanın kendi function’larını kullanır. Farkı yaratan, bu function’ları beklenmeyen sırada, beklenmeyen sıklıkta, yanlış aktör adına, çelişkili veriyle veya eşzamanlı biçimde çalıştırmasıdır.

Örnekler:

  • Müşterinin ürün fiyatını veya indirim tutarını request içinde belirleyebilmesi
  • Aynı kuponun veya promosyon hakkının birden fazla kez kullanılması
  • Kullanıcının başka tenant’a ait siparişi iade edebilmesi
  • Onay adımları tamamlanmadan işlemin final state’e taşınması
  • Transferi oluşturan kişinin kendi transferini onaylayabilmesi
  • Stok kontrolü ile stok düşme işlemi arasındaki yarış nedeniyle eksi stok oluşması
  • İptal edilmiş siparişe sonradan ödeme callback’i geldiğinde ürün teslimatının başlaması
  • Password reset veya para çekme gibi hassas flow’un sınırsız otomasyona açık olması
  • Refund toplamının orijinal tahsilatı aşabilmesi
  • Ücretsiz deneme hakkının yeni hesaplar veya alias’lar üzerinden sınırsız üretilmesi
  • Bir işlemin timeout sonrası client tarafından tekrar gönderilmesiyle iki kez gerçekleşmesi

Bu zafiyetler yalnızca finansal uygulamalara özgü değildir. Sağlık sisteminde hastanın başka bir doktorun onayına ihtiyaç duyan kaydı tek başına değiştirmesi, lojistik uygulamasında teslim edilmiş gönderinin tekrar “teslim alınmadı” state’ine döndürülmesi, SaaS ürününde lisans kotasının paralel request’lerle aşılması da business logic problemidir.

Legitimate functionality ne zaman zafiyete dönüşür?

Bir kullanıcının çok sayıda rezervasyon yapabilmesi bazı ürünlerde beklenen davranış, bazılarında inventory exhaustion saldırısı olabilir. Yorum yapmak için domain context gerekir.

Bir davranışı güvenlik bulgusu olarak değerlendirmek için en az şu üç unsur aranmalıdır:

  1. 1İhlal edilen kural: Uygulamanın koruması gereken açık bir business veya security invariant
  2. 2Kontrol edilebilir yol: Düşük yetkili ya da dış aktörün sonucu tetikleyebildiği işlem dizisi
  3. 3Anlamlı etki: Finansal kayıp, yetkisiz işlem, veri bütünlüğü bozulması, hizmet engelleme, fraud veya başka ölçülebilir zarar

“Bu kullanım bana garip geldi” tek başına finding değildir. Kural ürün sahibi, requirement, sözleşme, authorization matrix, state model veya başka güvenilir kaynakla doğrulanmalıdır.

CWE-840 neden doğrudan finding’e yazılmamalıdır?

MITRE, CWE-840 Business Logic Errors kaydını bir category olarak sınıflandırır ve gerçek vulnerability mapping’i için kullanılmasını önermez. Business logic, root cause’a göre daha somut weakness’lere ayrılmalıdır.

Örneğin:

Gözlenen problemDaha uygun root cause eşlemesi
Workflow adımlarının atlanabilmesiCWE-841 Improper Enforcement of Behavioral Workflow
Kullanıcı kontrollü nesne anahtarıyla authorization bypassCWE-639 Authorization Bypass Through User-Controlled Key
Tek kullanımlık işlemin birden fazla çalışmasıCWE-837 Improper Enforcement of a Single, Unique Action
Paylaşılan state üzerinde race conditionCWE-362 Concurrent Execution Using Shared Resource with Improper Synchronization
Check ile use arasında state’in değişebilmesiCWE-367 Time-of-check Time-of-use Race Condition
Kaynağın limitsiz veya throttling olmadan tüketilmesiCWE-770 Allocation of Resources Without Limits or Throttling
Behavioral workflow’un yanlış uygulanmasıCWE-841 Improper Enforcement of Behavioral Workflow

Doğru CWE seçimi yalnızca rapor düzeni değildir. Remediation’ın “business logic’i düzeltin” gibi belirsiz bir öneriden çıkıp gerçek root cause’a yönelmesini sağlar.

Race condition mapping’inde de mümkün olan en somut weakness seçilmelidir. CWE-362 daha üst seviyeli bir Class kaydıdır ve MITRE dikkatli kullanım ister. Kanıt gerçekten check ile use arasındaki state değişimini gösteriyorsa CWE-367 daha isabetli olabilir.

2. Business logic açıklarını SAST neden kolayca bulamaz?

SAST, code pattern, Abstract Syntax Tree, control flow ve data flow üzerinden güçlü sinyaller üretebilir. Kullanıcı girdisinin SQL query’ye ulaştığını, hardcoded secret bulunduğunu veya riskli deserialization function’ı kullanıldığını gösterebilir.

Business logic açığında ise tehlikeli olan satır çoğu zaman sıradan ve güvenlidir:

order.setStatus(OrderStatus.APPROVED);
orderRepository.save(order);

Bu kodun zafiyetli olup olmadığı şu bilgiler olmadan anlaşılamaz:

  • APPROVED state’ine kim geçebilir?
  • Önce hangi state’lerin tamamlanması gerekir?
  • Onaylayan kişi işlemi oluşturan kişi olabilir mi?
  • Tutar belirli eşiği aşıyorsa ikinci onay gerekir mi?
  • Fraud check tamamlanmadan onay verilebilir mi?
  • Aynı order için iki paralel onay ne üretir?
  • Bu method HTTP endpoint dışında message consumer veya scheduled job tarafından da çağrılıyor mu?

Syntax doğru, type doğru ve çağrılan API güvenlidir. Eksik olan domain kuralıdır.

Tool’un bilmediği şey “doğru sonuç”tur

SAST aşağıdaki data flow’u görebilir:

request.unitPrice -> BigDecimal.multiply -> payment.amount

Fakat unitPrice alanının user-controlled olmasının meşru mu yoksa kritik bir trust boundary ihlali mi olduğunu kendiliğinden bilemeyebilir. Marketplace satıcısının fiyat belirlemesi normal olabilir. Son müşterinin checkout sırasında fiyat belirlemesi normal değildir.

Aynı nedenle bir tool:

  • discount <= 100 kontrolünün teknik olarak var olduğunu görebilir
  • Yüzde 100 indirimin yalnızca çalışan kampanyasında izinli olduğunu bilemeyebilir
  • MANAGER role check’ini görebilir
  • Maker-checker kuralının aynı kullanıcının iki role birden sahip olmasını yasakladığını çıkaramayabilir
  • Transaction bulunduğunu görebilir
  • Uzak ödeme provider’ı ve local order update arasında partial failure riskini tam modelleyemeyebilir

SAST tamamen işe yaramaz mı?

Hayır. Business logic review’da SAST ve code search çok değerlidir. Amaç nihai kararı araca bırakmak değil, riskli pattern’leri geniş codebase boyunca bulmaktır.

Örneğin manuel inceleme, fiyatın client’tan alınmasının zafiyet olduğunu belirledikten sonra custom rule şu pattern’leri arayabilir:

  • Request DTO’dan price, amount, discount, fee, commission alanlarının domain entity’ye doğrudan taşınması
  • User-controlled tenantId, ownerId, accountId ile repository lookup
  • Request içinden gelen status değerinin entity state’ine doğrudan yazılması
  • exists veya count kontrolünden sonra ayrı insert
  • findById sonrası ownership filter’ı olmadan update veya delete
  • Tek kullanımlık token’ın validate edilip ayrı adımda consumed işaretlenmesi
  • @Async veya message consumer içinde transaction sınırının kaybolması

Business logic probleminde otomasyon çoğu zaman hypothesis üretir. Uzman review ise hypothesis’in domain açısından gerçek açık olup olmadığını belirler.

SAST nedir, hangi açıkları bulur ve hangilerini kaçırır? yazısında otomatik analizin business logic ve authorization alanındaki sınırlarını daha ayrıntılı inceleyebilirsiniz.

SAST, SCA, DAST ve manuel incelemenin aynı Secure SDLC içinde nasıl konumlandırılması gerektiğini Kaynak Kod Analizi (SAST) Nedir? Kapsamlı Rehber sayfasında bütünlüklü olarak ele alıyoruz.

OWASP Top 10:2025 A06 Insecure Design da eksik veya etkisiz control design ile implementation defect’i birbirinden ayırır. Gerekli kontrol tasarımda hiç oluşturulmadıysa kusursuz implementation bile o business rule’u koruyamaz.

3. Koddan önce business rule okunmalıdır

Reviewer yalnızca repository ile başlarsa implementation’ı görebilir, fakat implementation’ın neye göre doğru olması gerektiğini bilemez. Business logic review’ın ilk girdisi kod değil, beklenen davranış modelidir.

İnceleme öncesinde istenecek artifact’lar

  • Product requirement ve acceptance criteria
  • User story ve negatif senaryo tanımları
  • BPMN, sequence diagram veya workflow dokümanı
  • Role-permission ve maker-checker matrix’i
  • Tenant ve ownership modeli
  • State transition diagram
  • Fiyat, komisyon, limit, kota ve kampanya kuralları
  • Refund, cancellation ve dispute politikaları
  • API specification ve event schema
  • Database constraint ve transaction tasarımı
  • Fraud, sanction veya risk engine integration’ı
  • Retry, timeout ve compensation davranışı
  • Feature flag ve environment’a bağlı kurallar
  • Audit ve uyum gereksinimleri

Dokümantasyon eksikse review durmak zorunda değildir. Fakat eksik kural reviewer tarafından uydurulmamalıdır. Product owner, developer, fraud ekibi, finance veya ilgili business owner ile kısa bir threat modeling oturumu yapılarak varsayımlar doğrulanmalıdır.

OWASP ASVS 5.0.0 bu noktada ne ister?

OWASP ASVS 5.0.0, Validation and Business Logic bölümünü dokümantasyonla başlatır. V2.1 altında:

  • Tekil input’ların validation kuralları
  • Birbiriyle ilişkili verilerin logical consistency kuralları
  • Kullanıcı ve uygulama genelindeki business limit beklentileri

tanımlanır.

Bu sıralama bilinçlidir. Kural dokümante edilmeden tool, reviewer veya test otomasyonu hangi davranışın yanlış olduğunu aynı şekilde değerlendiremez.

Business invariant nasıl yazılır?

Invariant, işlem boyunca bozulmaması gereken kısa ve test edilebilir kuraldır.

Zayıf ifade:

Not

“İndirimler güvenli uygulanmalıdır.”

Test edilebilir invariant:

Not

“Siparişin toplam indirimi, kampanya tarafından belirlenen üst limiti ve ürün toplamını aşamaz. İndirim client tarafından belirlenemez. Aynı kupon aynı customer için yalnızca bir completed order üzerinde kullanılabilir.”

Başka örnekler:

  • Bir transferin toplam debit ve credit kaydı aynı currency’de eşit olmalıdır.
  • Refund toplamı captured payment tutarını aşamaz.
  • Bir tenant kullanıcısı başka tenant’ın object’ini okuyamaz veya değiştiremez.
  • SHIPPED order doğrudan DRAFT state’ine dönemez.
  • İşlemi oluşturan kullanıcı aynı işlemin final approver’ı olamaz.
  • Stok miktarı hiçbir committed transaction sonrasında sıfırın altına inemez.
  • Bir idempotency key aynı actor ve operation için tek business result üretmelidir.
  • Password reset token’ı yalnızca bir kez, doğru account ve geçerli süre içinde kullanılabilir.

Reviewer’ın görevi bu invariant’ların yalnızca happy path’te değil, bütün entry point ve failure mode’larda enforce edildiğini kanıtlamaktır.

4. Kritik business flow nasıl haritalanır?

Kod tabanını dosya sırasıyla okumak business logic review için verimsizdir. Önce en yüksek etkili flow seçilir, sonra bu flow’un entry point’ten kalıcı state değişikliğine kadar bütün execution path’i izlenir.

Önce para, yetki, veri ve sınırlı kaynak hareketlerini bulun

Yüksek riskli flow’lar genellikle şunlardan birini değiştirir:

  • Para, bakiye, kredi, puan veya indirim
  • Ownership, tenant veya role
  • Account recovery ve identity proof
  • Contract, approval veya safety override
  • Inventory, rezervasyon, kontenjan veya quota
  • Sensitive data paylaşımı veya export
  • Order, payment, shipment veya claim state’i
  • License, subscription veya entitlement
  • Credential, token, key veya trusted device

Bu flow’lar review backlog’unda CRUD ekranlarından önce gelmelidir.

Entry point yalnızca controller değildir

Aynı business service farklı kanallardan çağrılabilir:

  • REST veya GraphQL endpoint
  • WebSocket message
  • Message queue consumer
  • Scheduled job
  • Admin panel
  • Internal service endpoint
  • Batch import
  • Payment veya logistics webhook
  • Mobile-specific API
  • Customer support tool

HTTP controller’da bulunan bir authorization veya state check, message consumer’da yoksa kontrol bypass edilebilir. Bu nedenle call graph yalnızca Controller -> Service -> Repository şeklinde düşünülmemelidir.

Flow map hangi bilgileri içermelidir?

KatmanReviewer’ın çıkarması gereken bilgi
ActorCustomer, support, admin, service account, third-party provider
Entry pointEndpoint, event, job, webhook veya internal call
Identity sourceSession, JWT, mTLS identity, API key, event metadata
User-controlled dataObject ID, quantity, amount, status, tenant, callback field
Server-authoritative dataPrice, ownership, limit, current state, risk result
Decision pointAuthorization, eligibility, state transition, limit, approval
State writeDatabase update, ledger entry, event publish, external API call
Transaction boundaryHangi işlemler aynı transaction içinde, hangileri dışında
Failure pathTimeout, duplicate event, partial success, retry, rollback
Observable evidenceAudit event, metric, alert ve correlation ID

Harita çıkarıldığında kritik soru şudur:

Not

Bir saldırgan bu flow içinde hangi değeri, sırayı, zamanı, aktörü veya tekrar sayısını kontrol edebilir?

5. Trust boundary: Hangi veri gerçekten authoritative?

Business logic açığının en sık kök nedenlerinden biri, istemciden veya güvenilmeyen entegrasyondan gelen verinin business decision’da authoritative kabul edilmesidir.

Fiyatı client’tan almak

Zafiyetli örnek:

public record CheckoutItemRequest(
        UUID productId,
        int quantity,
        BigDecimal unitPrice,
        BigDecimal discount
) {}

@Transactional
public Order checkout(UUID customerId, List<CheckoutItemRequest> items) {
    BigDecimal total = items.stream()
            .map(item -> item.unitPrice()
                    .multiply(BigDecimal.valueOf(item.quantity()))
                    .subtract(item.discount()))
            .reduce(BigDecimal.ZERO, BigDecimal::add);

    return orderRepository.save(Order.create(customerId, items, total));
}

Kod:

  • Negative quantity kontrolü ekleyebilir
  • BigDecimal kullanabilir
  • SQL injection içermeyebilir
  • DTO validation’dan geçebilir

Yine de güvenli değildir. unitPrice ve discount saldırgan kontrollüdür.

Daha güvenli tasarım:

public record CheckoutItemRequest(
        UUID productId,
        int quantity,
        String campaignCode
) {}

@Transactional
public Order checkout(
        UUID authenticatedCustomerId,
        List<CheckoutItemRequest> requestedItems
) {
    List<PricedLine> lines = requestedItems.stream()
            .map(item -> {
                Product product = productRepository
                        .findSellableById(item.productId())
                        .orElseThrow(ProductNotAvailable::new);

                Quantity quantity = Quantity.of(item.quantity());
                Money base = product.currentPrice().multiply(quantity.value());
                Money discount = promotionService.calculateEligibleDiscount(
                        authenticatedCustomerId,
                        product,
                        quantity,
                        item.campaignCode()
                );

                return PricedLine.of(product.id(), quantity, base, discount);
            })
            .toList();

    Money total = pricingPolicy.calculateOrderTotal(lines);
    return orderRepository.save(Order.create(
            authenticatedCustomerId,
            lines,
            total
    ));
}

Bu örnekte önemli olan yalnızca alanları DTO’dan kaldırmak değildir:

  • Fiyat current product state’inden okunur.
  • İndirim eligibility’si authenticated customer üzerinden hesaplanır.
  • Quantity value object ile business range’e sokulur.
  • Order üzerinde fiyat snapshot’ı saklanır.
  • Payment daha sonra aynı order version ve amount ile bağlanmalıdır.

İstemciden gelen identity alanları

Şu signature risk sinyalidir:

public Refund refund(UUID userId, UUID orderId, BigDecimal amount)

userId request body veya path’ten geliyorsa method caller’ın kim olduğunu değil, caller’ın kimi iddia ettiğini kullanabilir.

Tercih edilen model:

public Refund refund(
        Actor actor,
        UUID orderId,
        Money requestedAmount
)

Actor, trusted authentication context’ten oluşturulmalı, request body’den deserialize edilmemelidir. Tenant, role, assurance level ve gerekli diğer authorization attribute’ları bu context üzerinden taşınabilir.

Webhook da otomatik olarak güvenilir değildir

Payment provider callback’inde:

  • Signature doğrulaması
  • Signature’ın doğru raw body üzerinde doğrulanması
  • Timestamp ve replay window
  • Provider account veya merchant binding
  • Event ID uniqueness
  • Amount ve currency’nin local order ile eşleşmesi
  • Expected state kontrolü

birlikte ele alınmalıdır.

Signature geçerliyse event provider’dan gelmiştir. Bu, local order’ın o event ile güncellenmesi gerektiğini tek başına kanıtlamaz.

6. Authorization yalnızca role kontrolü değildir

Business logic review sırasında “endpoint authenticated” veya “method üzerinde @PreAuthorize var” ifadesi authorization incelemesinin sonu değildir.

Bir işlem için şu kararlar ayrı ayrı doğrulanmalıdır:

  1. 1Actor bu function’ı çağırabilir mi?
  2. 2Actor bu object üzerinde işlem yapabilir mi?
  3. 3Object doğru tenant’a mı ait?
  4. 4Mevcut state bu işleme izin veriyor mu?
  5. 5Amount, region, product veya risk seviyesine bağlı ek approval gerekiyor mu?
  6. 6Actor işlemi oluşturan, onaylayan ve sonuçlandıran rollerden hangisinde?
  7. 7Delegation veya impersonation varsa asıl actor ve acting actor birlikte kontrol ediliyor mu?

findById sonrasında geç yapılan ownership kontrolü

Riskli pattern:

@PreAuthorize("hasRole('USER')")
public InvoiceDto getInvoice(UUID invoiceId) {
    Invoice invoice = invoiceRepository.findById(invoiceId)
            .orElseThrow(InvoiceNotFound::new);

    return mapper.toDto(invoice);
}

Role check, kullanıcının herhangi bir invoice okuma function’ına erişebildiğini gösterir. İlgili invoice’ın bu kullanıcıya veya tenant’a ait olduğunu göstermez.

Repository sorgusuna security context eklemek daha güçlü bir default sağlar:

public InvoiceDto getInvoice(Actor actor, UUID invoiceId) {
    Invoice invoice = invoiceRepository
            .findByIdAndTenantIdAndCustomerId(
                    invoiceId,
                    actor.tenantId(),
                    actor.subjectId()
            )
            .orElseThrow(InvoiceNotFound::new);

    return mapper.toDto(invoice);
}

Bu yaklaşımın da sınırları vardır. Support user, delegated access veya group ownership gibi modellerde basit customerId filtresi yeterli olmayabilir. Önemli olan ownership kararının açık, merkezi ve deny-by-default uygulanmasıdır.

IDOR açığının neden hâlâ bu kadar yaygın olduğunu ele aldığımız yazıda object-level authorization’ın controller, service ve data access katmanlarındaki uygulamasını daha geniş örneklerle inceleyebilirsiniz.

Maker-checker kontrolü role matrix’inden fazlasıdır

Şu kod iki role’ü kontrol ediyor gibi görünür:

@PreAuthorize("hasRole('APPROVER')")
public void approve(UUID transferId) {
    Transfer transfer = transferRepository.findById(transferId)
            .orElseThrow(TransferNotFound::new);

    transfer.approve();
}

Eksik invariant:

transfer.createdBy != currentActor.id

Ayrıca high-value transfer için farklı business unit, ikinci approver veya stronger authentication gerekebilir. Role doğru olsa bile actor-object ilişkisi yanlış olabilir.

7. Workflow ve state transition açıkları nasıl bulunur?

Uygulamalar sık sık işlemin doğru sırayla ilerleyeceğini frontend’e bırakır. Kullanıcı arayüzü yalnızca geçerli butonu gösterir, fakat backend herhangi bir state’ten final action’ı kabul eder.

OWASP WSTG Business Logic Testing, step skipping, request forgery, integrity check, process timing, function kullanım sayısı ve workflow circumvention gibi başlıkları ayrı test alanları olarak ele alır.

Request’ten status almak

Riskli örnek:

public record UpdateOrderRequest(
        OrderStatus status,
        String note
) {}

@Transactional
public void updateOrder(UUID orderId, UpdateOrderRequest request) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(OrderNotFound::new);

    order.setStatus(request.status());
    order.setNote(request.note());
}

Bu modelde API caller, domain state machine’i yönetir. DRAFT sipariş doğrudan DELIVERED yapılabilir veya REFUNDED order tekrar PAID state’ine taşınabilir.

State’i alan olarak yazmak yerine domain action tanımlanmalıdır:

@Transactional
public void markAsShipped(Actor actor, UUID orderId, ShipmentProof proof) {
    Order order = orderRepository.findForUpdate(orderId)
            .orElseThrow(OrderNotFound::new);

    authorization.checkCanShip(actor, order);
    order.markAsShipped(proof, clock.instant());
}

Domain entity geçişi enforce etmelidir:

public void markAsShipped(ShipmentProof proof, Instant now) {
    if (status != OrderStatus.PAID) {
        throw new InvalidOrderTransition(status, OrderStatus.SHIPPED);
    }
    if (paymentReference == null) {
        throw new PaymentNotConfirmed();
    }
    if (proof == null || !proof.isValid()) {
        throw new InvalidShipmentProof();
    }

    this.status = OrderStatus.SHIPPED;
    this.shippedAt = now;
}

State machine tablosu çıkarın

Current stateİzinli actionNext stateActorEk invariant
DRAFTsubmitPENDING_PAYMENTCustomerSepet boş değil, fiyat snapshot güncel
PENDING_PAYMENTconfirmPaymentPAIDPayment serviceAmount, currency, merchant ve event eşleşiyor
PAIDshipSHIPPEDFulfillmentStock reserve edilmiş, shipment proof geçerli
SHIPPEDdeliverDELIVEREDCarrier webhookTracking ve carrier binding doğru
PAIDcancelCANCELLEDCustomer/SupportShipment başlamamış
DELIVEREDrequestRefundREFUND_PENDINGCustomerRefund window açık
REFUND_PENDINGapproveRefundREFUNDEDFinanceRefund toplamı captured amount’ı aşmıyor

Ardından her state için şu negatif yollar aranır:

  • Step atlama
  • Eski step’e geri dönme
  • Aynı step’i tekrar etme
  • İki farklı step’i paralel çalıştırma
  • Farklı actor ile devam etme
  • Başka flow’dan üretilmiş token veya object kullanma
  • Timeout veya callback sonrası stale state ile işlem yapma

UI’da görünmeyen endpoint unutulmamalıdır

Legacy mobile endpoint, internal API veya önceki version route’u aynı domain action’a farklı validation ile ulaşabilir. Kod incelemesinde:

markAsPaid(
approve(
refund(
cancel(
setStatus(
transitionTo(

gibi state-changing method’ların bütün caller’ları aranmalıdır. Tek bir güvenli controller, bütün call path’lerin güvenli olduğunu göstermez.

8. Hesaplama ve parasal invariant’lar

Fiyat manipülasyonu yalnızca client-controlled price değildir. Para ve kota hesaplarında rounding, currency, sign, upper bound ve operation order hataları business impact doğurabilir.

İncelenecek kurallar

  • Amount negatif, sıfır veya beklenenden büyük olabilir mi?
  • Quantity ile price multiplication overflow veya scale problemi üretir mi?
  • Currency conversion hangi kur ve timestamp ile yapılıyor?
  • Discount tax öncesi mi sonrası mı uygulanmalı?
  • Birden fazla promotion stack edilebilir mi?
  • Refund fee dahil mi, hariç mi hesaplanıyor?
  • Rounding line-level mı, order-level mı yapılmalı?
  • Toplam discount ürün toplamını aşabilir mi?
  • Credit veya points negative balance oluşturabilir mi?
  • Percentage 0-100 aralığında olsa bile actor o percentage’i belirleyebilir mi?

Floating-point yerine BigDecimal kullanmak tek başına çözüm değildir

Şu kullanım beklenmeyen sonuç üretebilir:

BigDecimal amount = new BigDecimal(0.1);

Binary floating-point değeri constructor’a taşınır. Para için string veya valueOf tercih edilir:

BigDecimal amount = new BigDecimal("0.10");

Ancak type seçimi business invariant’ı enforce etmez. BigDecimal("-1000.00") teknik olarak geçerli bir değerdir.

Money value object:

public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);

        int fractionDigits = currency.getDefaultFractionDigits();
        if (fractionDigits >= 0 && amount.scale() > fractionDigits) {
            throw new IllegalArgumentException("Unsupported monetary scale");
        }
    }

    public Money add(Money other) {
        requireSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }

    public boolean isNegative() {
        return amount.signum() < 0;
    }
}

Value object ortak kuralları merkezileştirir. “Refund negatif olamaz” veya “credit limit’i aşamaz” gibi operation-specific invariant’lar yine domain service içinde uygulanmalıdır.

Aggregate toplamını database constraint ile de koruyun

Application check gerekli olsa da mümkün olan invariant’lar database seviyesinde desteklenmelidir:

ALTER TABLE account
ADD CONSTRAINT chk_account_balance
CHECK (balance >= 0);

Her domain negative balance’ı yasaklamaz. Overdraft hesabında bu constraint yanlış olabilir. Constraint, yalnızca doğrulanmış business rule’a göre eklenmelidir.

9. Tek kullanımlık işlemler, replay ve idempotency

Network timeout olduğunda client response’u alamaz ve aynı request’i tekrar gönderir. Message broker aynı event’i yeniden teslim edebilir. Kullanıcı butona iki kez basabilir. Saldırgan da aynı işlemi bilinçli olarak replay edebilir.

İşlem “bir kez yapılmalı” kuralına sahipse idempotency tasarımın parçası olmalıdır.

Riskli check-then-act

if (!couponUseRepository.existsByCustomerIdAndCouponId(customerId, couponId)) {
    applyCoupon(order, couponId);
    couponUseRepository.save(new CouponUse(customerId, couponId));
}

İki paralel transaction aynı anda exists == false görebilir ve ikisi de indirimi uygulayabilir.

Sadece application check’e güvenmek yerine unique constraint gerekir:

ALTER TABLE coupon_use
ADD CONSTRAINT uq_coupon_use_customer
UNIQUE (customer_id, coupon_id);

Fakat constraint’in hangi anda yazıldığı önemlidir. İndirim order’a uygulanıp coupon use kaydı daha sonra ayrı transaction’da yazılırsa partial failure oluşabilir. İlgili local state değişiklikleri tek transaction içinde olmalıdır.

Idempotency key nasıl tasarlanmalıdır?

Sadece cache’e “bu key görüldü” yazmak yeterli değildir. Güçlü bir model:

  • Key’i actor, operation ve uygun business scope ile bağlar.
  • İlk request’in canonical hash’ini saklar.
  • Aynı key farklı payload ile gelirse reddeder.
  • Business result veya stable response’u saklar.
  • Unique constraint ile yarışa dayanır.
  • PROCESSING, SUCCEEDED, FAILED_RETRYABLE gibi state’leri tanımlar.
  • Retention süresini operation riskine göre belirler.
  • Downstream side effect’leri de aynı idempotency context ile yönetir.

Örnek tablo:

CREATE TABLE idempotency_record (
    actor_id UUID NOT NULL,
    operation VARCHAR(80) NOT NULL,
    idempotency_key VARCHAR(120) NOT NULL,
    request_hash CHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    resource_id UUID,
    response_body JSONB,
    created_at TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (actor_id, operation, idempotency_key)
);

Idempotent endpoint ile exactly-once aynı şey değildir

Distributed system’de exactly-once guarantee iddiası dikkatle kullanılmalıdır. Local database transaction ile external provider çağrısı aynı atomic boundary içinde değildir. Request idempotency, transactional outbox, consumer deduplication ve provider idempotency desteği birlikte gerekebilir.

Örneğin payment provider charge işlemi başarılı olur, local transaction timeout nedeniyle rollback olur. Client tekrar deneyince ikinci charge oluşmaması için provider’a aynı idempotency key gönderilmeli ve local reconciliation yapılmalıdır.

10. Race condition ve TOCTOU açıkları kodda nasıl yakalanır?

Race condition, aynı shared state’e birden fazla execution path’in beklenmeyen sırayla erişmesi sonucunda invariant’ın bozulmasıdır. Business logic tarafında stok, bakiye, kullanım hakkı, limit, rezervasyon ve tek kullanımlık token’larda sık görülür.

Check ile update ayrıysa yarış penceresi oluşabilir

Zafiyetli stok örneği:

@Transactional
public Reservation reserve(UUID productId, int quantity) {
    Inventory inventory = inventoryRepository.findByProductId(productId)
            .orElseThrow(InventoryNotFound::new);

    if (inventory.getAvailable() < quantity) {
        throw new InsufficientInventory();
    }

    inventory.setAvailable(inventory.getAvailable() - quantity);
    return reservationRepository.save(
            Reservation.create(productId, quantity)
    );
}

İki request aynı available = 1 değerini okuyup quantity = 1 kontrolünü geçebilir. Her ikisi de rezervasyon oluşturursa tek ürün iki kez satılmış olur. Isolation level, ORM behavior ve update statement’a göre final stok 0 görünebilir. Bu durum oversell gerçekleşmediği anlamına gelmez. Lost update, ikinci rezervasyon kaydı üzerinden etkisini gösterebilir.

Atomic conditional update

Belirli invariant’lar tek database statement ile enforce edilebilir:

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
    update Inventory i
       set i.available = i.available - :quantity
     where i.productId = :productId
       and i.available >= :quantity
""")
int reserveIfAvailable(UUID productId, int quantity);

Service:

@Transactional
public Reservation reserve(UUID productId, int quantity) {
    Quantity requested = Quantity.of(quantity);

    int updated = inventoryRepository.reserveIfAvailable(
            productId,
            requested.value()
    );
    if (updated != 1) {
        throw new InsufficientInventory();
    }

    return reservationRepository.save(
            Reservation.create(productId, requested.value())
    );
}

updated == 1 hem check hem write sonucudur. Arada başka transaction’ın state’i değiştirebileceği bir pencere yoktur.

Bulk update persistence context’i bypass edebileceği için aynı transaction içinde stale entity kullanımı ayrıca değerlendirilmelidir. clearAutomatically yardımcı olabilir, fakat aggregate’in geri kalanıyla consistency ihtiyacı tasarıma göre incelenmelidir.

Optimistic locking

Entity üzerinde @Version kullanmak lost update’i fark edebilir:

@Entity
public class Inventory {
    @Id
    private UUID productId;

    @Version
    private long version;

    private int available;

    public void reserve(int quantity) {
        if (quantity <= 0 || available < quantity) {
            throw new InsufficientInventory();
        }
        available -= quantity;
    }
}

İki transaction aynı version’ı okursa ilk commit version’ı artırır, ikincisi OptimisticLockException alır. Buradaki kritik noktalar:

  • Exception sessizce success’e çevrilmemelidir.
  • Retry yapılıyorsa invariant yeni state üzerinde yeniden değerlendirilmelidir.
  • Sınırsız retry yeni DoS veya duplicate side effect üretmemelidir.
  • External API çağrısı retry’den önce gerçekleşmişse duplicate işlem riski vardır.

Pessimistic locking ne zaman?

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select i from Inventory i where i.productId = :productId")
Optional<Inventory> findForUpdate(UUID productId);

Pessimistic lock yüksek contention altında throughput ve deadlock riski oluşturabilir. Lock doğru row ve doğru transaction boundary üzerinde tutulmalıdır. Uzak servis çağrısı boyunca database lock tutmak çoğu zaman kötü tasarımdır.

Tek bir evrensel çözüm yoktur:

  • Atomic conditional update kısa ve güçlü olabilir.
  • Optimistic locking düşük contention için uygundur.
  • Pessimistic locking belirli kritik section’larda gerekebilir.
  • Queue veya partition-based serialization bazı yüksek değerli flow’larda tercih edilebilir.
  • Ledger tasarımında append-only entry ve balance constraint farklı bir model sunabilir.

Reviewer’ın sorusu “lock var mı?” değil, “invariant bütün concurrent interleaving’lerde korunuyor mu?” olmalıdır.

TOCTOU yalnızca file system problemi değildir

CWE-367, bir resource’un check ile use arasında değişebilmesini tanımlar. Application business logic’te benzer pattern:

limit kontrolü -> başka işlem limiti tüketir -> transfer gerçekleşir
token geçerlilik kontrolü -> başka request token’ı tüketir -> token tekrar kullanılır
order state kontrolü -> callback order’ı iptal eder -> shipment başlatılır

Check ve action aynı güvenilir transaction veya atomic operation içinde değilse kontrol yalnızca eski state’i kanıtlar.

Race condition review için pratik code search

Java codebase’inde aşağıdaki aramalar başlangıç sinyali sağlayabilir:

rg -n "existsBy|countBy|findById|getAvailable|getBalance|getLimit" src/
rg -n "@Transactional|@Version|PESSIMISTIC|OPTIMISTIC|LockModeType" src/
rg -n "@Async|CompletableFuture|Executor|KafkaListener|RabbitListener" src/
rg -n "save\\(|delete\\(|setStatus|setBalance|setAvailable" src/

Bu sonuçlar finding değildir. Reviewer check-then-act, read-modify-write, side effect sırası ve transaction propagation ilişkisini araştırmak için kullanır.

11. Transaction sınırı ve partial failure

Bir method üzerinde @Transactional görmek business operation’ın atomic olduğunu kanıtlamaz. Transaction çoğunlukla yalnızca aynı database resource üzerindeki local değişiklikleri kapsar. E-posta, message broker, payment provider, object storage veya başka mikroservis aynı atomic boundary içinde değildir.

Yanlış side effect sırası

@Transactional
public void completeOrder(UUID orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(OrderNotFound::new);

    shippingClient.createShipment(order);
    order.markCompleted();
}

createShipment başarılı olduktan sonra database commit başarısız olursa shipment oluşur, order eski state’te kalır. Retry ikinci shipment’ı üretebilir.

Tersi sıra da tek başına çözüm değildir:

order.markCompleted();
shippingClient.createShipment(order);

Uzak çağrı başarısız olursa transaction rollback olabilir, fakat remote system rollback olmaz. Ayrıca uzun remote call boyunca database transaction açık kalır.

Transactional outbox

Local state ve yayınlanacak event aynı database transaction’ında kaydedilebilir:

@Transactional
public void completeOrder(UUID orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(OrderNotFound::new);

    order.markReadyForShipment();
    outboxRepository.save(OutboxEvent.shipmentRequested(order));
}

Ayrı publisher outbox event’ini broker’a yollar. Bu model event kaybı riskini azaltır, fakat consumer’ın duplicate event’e dayanıklı olması yine gerekir.

Review sırasında:

  • Outbox row ile domain change aynı transaction’da mı?
  • Publisher crash sonrası event tekrar gönderiliyor mu?
  • Consumer event ID’yi deduplicate ediyor mu?
  • Duplicate geldiğinde side effect tekrar oluşuyor mu?
  • Event ordering gerekli mi ve partition key doğru mu?
  • Poison message business flow’u kalıcı olarak durduruyor mu?
  • Dead-letter queue owner’ı ve reconciliation süreci var mı?

soruları sorulmalıdır.

Compensation her zaman rollback değildir

Saga içinde bir adım başarısız olduğunda önceki adımın tersini çalıştırmak gerekebilir. Fakat bütün işlemler tam tersine çevrilemez. Gönderilmiş e-posta geri alınamaz, dışarı aktarılmış veri silinse bile görülmüş olabilir, finansal reversal yeni bir ledger entry gerektirebilir.

Compensation flow da:

  • Authorization
  • Idempotency
  • State validation
  • Audit
  • Retry
  • Manual intervention

açısından ayrı bir business flow olarak incelenmelidir.

Spring @Transactional için code review tuzakları

Java Spring projelerinde şu durumlar özellikle kontrol edilmelidir:

  • Aynı class içindeki self-invocation nedeniyle proxy’nin devreye girmemesi
  • Method’un private olması veya proxy modeline uymaması
  • Checked exception’da beklenen rollback’in gerçekleşmemesi
  • REQUIRES_NEW ile outer transaction’dan bağımsız commit
  • @Async ile transaction context’inin başka thread’e taşınmaması
  • Lazy loading veya entity state’in transaction dışında kullanılması
  • Event listener’ın commit öncesi çalışıp external side effect üretmesi
  • @TransactionalEventListener phase seçiminin iş kuralıyla uyuşmaması
  • Bulk update sonrası stale persistence context

Java Spring projelerinde güvenlik kod incelemesine nereden başlanır? yazısında controller’dan repository’ye kadar framework’e özgü review sırasını ayrıntılı olarak ele aldık.

12. Anti-automation ve kullanım limiti

Business flow, tek bir request için tamamen doğru olabilir. Aynı function binlerce kez çağrıldığında fraud, inventory exhaustion veya maliyet üretmesi ayrı bir güvenlik problemidir.

OWASP API Security Top 10 API6:2023, sensitive business flow’ların aşırı kullanımına örnek olarak bütün stokların bot ile alınmasını, rezervasyon slot’larının kapatılmasını ve referral program’ın sahte hesaplarla kötüye kullanılmasını gösterir.

Rate limit neden tek başına yetmeyebilir?

IP başına dakikada 10 request:

  • Dağıtık IP kullanımıyla aşılabilir
  • Farklı account’larla bölünebilir
  • IPv6 address rotation ile etkisizleşebilir
  • Aynı business object üzerinde çok sayıda identity tarafından kötüye kullanılabilir
  • Düşük hızlı fakat uzun süreli abuse’u kaçırabilir

Limit birden fazla key ve time window üzerinden düşünülmelidir:

  • Actor
  • Account
  • Device
  • Tenant
  • Payment instrument
  • Phone veya verified identity
  • Business object
  • Destination account
  • Campaign
  • Global inventory

Limitin kendisi business rule’dur

“Kullanıcı günde üç kez para çekebilir” gibi bir limit kodda sabit sayı olarak dağılmışsa environment ve kanal tutarsızlığı oluşabilir.

Reviewer:

  • Limitin authoritative configuration kaynağını
  • Timezone ve window reset davranışını
  • Limit değişikliğinin audit’ini
  • Başarısız işlemlerin limiti tüketip tüketmediğini
  • Mobile, web ve support kanallarının aynı counter’ı kullanıp kullanmadığını
  • Refund veya cancellation ile counter’ın geri açılıp açılmadığını
  • Distributed cache kaybında fail-open davranışı

inceler.

CAPTCHA business control yerine geçmez

CAPTCHA bot maliyetini artırabilir. Kritik flow’un tek kontrolü olmamalıdır. Inventory reservation için per-account limit, payment instrument binding, verified identity, queue, waiting room, risk scoring ve purchase finalization süreleri birlikte kullanılabilir.

ASVS 5.0.0 ile ilişki

ASVS v5.0.0:

  • V2.3.2 ile dokümante business logic limitlerinin enforce edilmesini
  • V2.4.1 ile aşırı function çağrılarının data exfiltration, quota exhaustion, DoS ve costly resource abuse üretmesine karşı anti-automation control’lerini
  • V2.4.2 ile yüksek güvence gereken flow’larda gerçekçi insan timing’inin dikkate alınmasını

ister.

Bu requirement’lar generic “rate limit ekleyin” önerisinden daha geniştir. Limit, korunan business sonucu ve saldırganın dağıtım kabiliyetiyle tasarlanmalıdır.

13. Kod incelemesinde uygulanabilir business logic yöntemi

Business logic review, reviewer’ın sezgisine tamamen bağımlı bırakılırsa coverage ölçülemez. Aşağıdaki yöntem kritik flow başına tekrarlanabilir bir çalışma sunar.

Bu derinlik, otomatik finding üretiminin ötesinde application context ve uzman muhakemesi gerektirir. İki yaklaşımın görev paylaşımını Secure Code Review ile otomatik SAST taraması farkı yazısında teknik coverage üzerinden karşılaştırıyoruz.

Adım 1: Kritik objective’i seçin

“Sipariş modülünü incele” yerine:

  • Ürünün doğru fiyatla tek kez satın alınması
  • Refund’ın captured amount’ı aşmaması
  • Transferin doğru owner ve approval chain ile tamamlanması
  • Bir tenant’ın başka tenant state’ini etkileyememesi

gibi test edilebilir objective tanımlayın.

Adım 2: Actor ve yetki ilişkisini çıkarın

Her flow için:

  • Anonymous user
  • Authenticated customer
  • Support
  • Operator
  • Approver
  • Administrator
  • Service account
  • Third-party provider

rollerini çıkarın. Yalnızca role adı değil, actor’ın object ve önceki action’larla ilişkisi kaydedilmelidir.

Adım 3: Invariant listesini yazın

Happy path dokümanı, güvenlik requirement’ına çevrilir:

INV-ORDER-01: Total server-side price snapshot’tan hesaplanır.
INV-ORDER-02: Discount toplamı ürün toplamını aşamaz.
INV-ORDER-03: Aynı coupon customer başına bir completed order’da kullanılabilir.
INV-ORDER-04: PAID olmayan order shipment üretemez.
INV-ORDER-05: Aynı idempotency key tek order üretir.

Invariant ID’leri code comment içine yaymak şart değildir. Review worksheet, test case ve raporda aynı ID’leri kullanmak traceability sağlar.

Adım 4: Bütün entry point’leri bulun

Controller annotation’ları, message listener’lar, scheduled job’lar, GraphQL resolver’lar, admin action’ları ve webhook’lar listelenir. Aynı domain method’a ulaşan bütün caller’lar karşılaştırılır.

Adım 5: User-controlled ve server-authoritative veriyi ayırın

Request DTO’daki her field için:

  • Actor bunu belirleyebilir mi?
  • Backend tekrar hesaplıyor mu?
  • Object’ten mi okunmalı?
  • Identity context’ten mi gelmeli?
  • Signature geçerli olsa bile local state ile eşleşmesi gerekiyor mu?

soruları cevaplanır.

Adım 6: Decision ve enforcement point’i eşleştirin

Her invariant’ın kodda nerede enforce edildiği kaydedilir:

InvariantEnforcement pointEk korumaTest
Başka tenant invoice okuyamazRepository tenant predicateService authorization policyCross-tenant negative integration test
Refund captured amount’ı aşamazRefund aggregateDB/ledger reconciliationPartial ve parallel refund test
Coupon tek kullanımlıkUnique constraint + transactionIdempotency recordConcurrent replay test
Maker kendi işlemini onaylayamazApproval policyAudit alertSame-actor negative test

Kural yalnızca UI’da veya API gateway’deyse backend alternate entry point üzerinden bypass olabilir.

Adım 7: State machine ve geçiş caller’larını inceleyin

Generic setStatus, update, patch method’ları risklidir. Domain action ve transition guard aranır. Her current state’ten her endpoint’in çağrılması halinde ne olacağı düşünülür.

Adım 8: Transaction ve side effect sırasını çıkarın

Database write, event publish ve remote call’ın sırası bir sequence diagram üzerinde gösterilir. Her adım sonrasında process crash olduğu varsayılır:

  • Hangi state kalır?
  • Retry ne yapar?
  • Duplicate oluşur mu?
  • Reconciliation bunu bulur mu?

Adım 9: Concurrent ve replay execution düşünün

Her “önce kontrol et, sonra yaz” pattern’i için iki paralel request düşünülür. Tek kullanımlık token, stock, balance, quota ve approval state’i özellikle incelenir.

Adım 10: Abuse case’i test case’e dönüştürün

Finding yalnızca code observation olarak bırakılmaz. Yetkili test environment’ında veya güvenli unit/integration test ile doğrulanır:

  • Step skip
  • Repeat
  • Reorder
  • Parallel execution
  • Cross-user
  • Cross-tenant
  • Stale state
  • Boundary value
  • Partial failure

Adım 11: Root cause yayılımını araştırın

Tek endpoint düzeltmesi yeterli olmayabilir. Aynı DTO mapper, repository helper, authorization policy veya state update pattern’i repository genelinde aranır.

Adım 12: Remediation’ı regression control’e çevirin

Kalıcı kapanış:

  • Domain invariant
  • Database constraint
  • Negative test
  • Concurrency test
  • Custom SAST rule
  • Detection veya audit alert

ile desteklenmelidir.

14. Java Spring codebase’inde hangi pattern’ler incelenmelidir?

Framework bilgisi business logic context’in yerini almaz. Yine de belirli code pattern’leri review başlangıcını hızlandırır.

Request DTO’dan entity’ye toplu mapping

BeanUtils.copyProperties(request, entity);

veya:

modelMapper.map(request, entity);

Bu kullanım:

  • status
  • ownerId
  • tenantId
  • price
  • approved
  • role
  • balance

gibi server-controlled alanların overwrite edilmesine yol açabilir. Mapping allowlist ile sınırlandırılmalı, sensitive state domain action’larla değişmelidir.

Generic update method’ları

public Entity update(UUID id, Map<String, Object> fields)

ve JSON Merge Patch implementation’ları property-level authorization açısından incelenmelidir. Actor bir object’i update edebiliyor diye her property’yi update edebilmemelidir.

Repository method adlarında identity eksikliği

Risk sinyalleri:

findById(id)
deleteById(id)
findAllByStatus(status)
updateStatus(id, status)

Multi-tenant uygulamada güvenli query çoğu zaman tenant veya ownership predicate’i taşımalıdır:

findByIdAndTenantId(id, tenantId)

Bu yalnızca naming convention değildir. Custom query, specification ve native SQL’de aynı restriction’ın korunduğu doğrulanmalıdır.

Controller’daki güvenlik kontrolü

if (!currentUser.id().equals(request.userId())) {
    throw new Forbidden();
}
service.execute(request);

Service başka controller, listener veya internal call’dan çağrıldığında check atlanabilir. Kritik authorization ve invariant trusted service veya domain boundary içinde enforce edilmelidir.

@PreAuthorize expression’ları

Şu başlıklar incelenir:

  • Role prefix ve authority naming tutarlılığı
  • Method parameter ile expression binding
  • Proxy’nin gerçekten devreye girip girmediği
  • Same-class call ile method security bypass
  • Meta-annotation’ların doğru composition’ı
  • Default-deny yerine annotation unutulmasına açık tasarım
  • Object ownership kararının yalnızca role’e indirgenmesi

Event listener ve scheduler

@KafkaListener
public void handle(PaymentConfirmed event) { ... }

Event:

  • Hangi producer’dan gelebilir?
  • Tenant ve merchant binding’i var mı?
  • Duplicate olabilir mi?
  • Out-of-order gelebilir mi?
  • Eski version event current state’i bozabilir mi?
  • Schema’daki amount local order ile eşleşiyor mu?

diye incelenmelidir.

Feature flag

Flag yalnızca UI’yı kapatıp backend endpoint’i açık bırakabilir. Ayrıca flag evaluation user-controlled attribute’a dayanıyorsa entitlement bypass oluşabilir. Default value, cache outage, rollout rule ve admin override audit’i kontrol edilmelidir.

15. Business logic açıkları testlerle nasıl kalıcı olarak yakalanır?

Manuel review bulguyu keşfeder. Regression test aynı hatanın geri gelmesini önler. Happy path test’leri business logic güvenliği için yeterli değildir.

Negative unit test

@Test
void creatorCannotApproveOwnTransfer() {
    Actor maker = actor("user-17", Role.APPROVER);
    Transfer transfer = transferCreatedBy("user-17");

    assertThatThrownBy(() -> transfer.approveBy(maker))
            .isInstanceOf(SelfApprovalNotAllowed.class);
}

Bu test role’ün varlığını değil, actor-object relationship invariant’ını doğrular.

State transition test

@ParameterizedTest
@EnumSource(
        value = OrderStatus.class,
        names = {"DRAFT", "CANCELLED", "REFUNDED"}
)
void orderCannotShipFromInvalidState(OrderStatus state) {
    Order order = orderInState(state);

    assertThatThrownBy(() -> order.markAsShipped(validProof(), now()))
            .isInstanceOf(InvalidOrderTransition.class);
}

Tek bir current state yerine yasaklı state set’i test edilir.

Concurrent integration test

Concurrency test deterministik tasarlanmalıdır. Yalnızca iki thread başlatıp tesadüfen race beklemek flaky test üretir. Barrier kullanılarak iki execution aynı noktaya getirilir:

@Test
void lastInventoryItemCanOnlyBeReservedOnce() throws Exception {
    UUID productId = inventoryWithAvailableQuantity(1);
    CyclicBarrier barrier = new CyclicBarrier(2);

    Callable<Result> attempt = () -> {
        barrier.await();
        return reservationFacade.tryReserve(productId, 1);
    };

    List<Future<Result>> futures = executor.invokeAll(
            List.of(attempt, attempt)
    );

    long successes = futures.stream()
            .map(this::getUnchecked)
            .filter(Result::isSuccess)
            .count();

    assertThat(successes).isEqualTo(1);
    assertThat(inventory(productId).available()).isZero();
    assertThat(reservationsFor(productId)).hasSize(1);
}

Test sadece final stock değerini değil, oluşan reservation sayısını da doğrular.

Property-based test

Finansal invariant birçok input combination’ında test edilebilir:

Her geçerli cart için:
total >= 0
discount <= subtotal
refundSum <= capturedAmount
debitSum == creditSum

Property-based testing, örnek bazlı testlerin düşünmediği boundary combination’larını üretebilir. Generator gerçek business constraint’leri taşımalı, aksi halde yalnızca anlamsız input gürültüsü oluşur.

Model-based state machine test

Expected state model test tarafında tanımlanır. Random action sequence hem model’e hem uygulamaya uygulanır. Uygulamanın kabul veya red kararı model ile karşılaştırılır.

Bu yöntem:

  • Step skipping
  • Invalid rollback
  • Repeat
  • Unexpected terminal-state transition
  • Action ordering

problemlerini bulmak için güçlüdür.

Contract ve consumer test

Microservice flow’unda producer ile consumer’ın aynı event semantics’i kullandığı doğrulanmalıdır. paymentConfirmed event’inin “provider callback alındı” mı, “amount ve merchant doğrulandı” mı anlamına geldiği açık değilse consumer güvenilmeyen state’i final kabul edebilir.

Event isimleri yalnızca teknik bildirim değil, doğrulanmış business fact ifade etmelidir.

16. Finding nasıl raporlanmalı ve nasıl düzeltilmeli?

“Business logic error bulundu” geliştirici için yeterli değildir. Finding, ihlal edilen invariant’ı ve saldırganın hangi işlem dizisiyle sonuca ulaştığını göstermelidir.

Rapor içeriği

  1. 1Başlık: Etkiyi ve root cause’u anlatan kısa ifade
  2. 2Etkilenen flow: Endpoint, service, consumer ve ilgili component’ler
  3. 3İhlal edilen invariant: Uygulamanın koruması gereken kural
  4. 4Precondition: Gerekli role, account, state ve timing
  5. 5Attack sequence: Replay edilebilir adımlar
  6. 6Evidence: Request/response, code path, database state ve log
  7. 7Business impact: Fraud, financial loss, unauthorized action veya integrity etkisi
  8. 8Root cause: Trust boundary, workflow, concurrency, authorization veya transaction hatası
  9. 9Yayılım: Aynı pattern’in diğer flow’lardaki örnekleri
  10. 10Remediation: Architecture ve implementation değişikliği
  11. 11Regression plan: Negative, concurrency veya state test’i
  12. 12CWE: Category yerine doğrulanmış root cause’a uygun eşleme

Zayıf ve güçlü başlık örneği

Zayıf:

Not

Business Logic Vulnerability

Güçlü:

Not

Paralel Request’ler Aynı Promosyon Hakkının Birden Fazla Siparişte Kullanılmasına İzin Veriyor

İkinci başlık reviewer, developer ve risk owner’a aynı anda neyin bozulduğunu anlatır.

Remediation yalnızca endpoint’e if eklemek değildir

Tek endpoint’e eklenen check:

  • Başka entry point’te unutulabilir
  • Concurrent execution’da yine yarışabilir
  • Background job tarafından bypass edilebilir
  • Yeni feature’da tekrar oluşabilir

Root cause’a göre çözüm:

  • Domain invariant’ı aggregate içinde enforce etmek
  • Authorization predicate’i data access’e taşımak
  • Unique veya check constraint eklemek
  • Atomic update veya locking kullanmak
  • State-specific command tasarlamak
  • Idempotency record ve request binding kurmak
  • Transactional outbox ve consumer deduplication uygulamak
  • Negative ve concurrency test eklemek

olabilir.

Severity nasıl belirlenmeli?

Business logic bulgusunda CVSS tek başına business impact’i yeterli ayrıntıyla anlatmayabilir. Yine de attack precondition, required privilege, user interaction, scope ve confidentiality/integrity/availability etkisi teknik severity için değerlendirilir.

Buna ek olarak:

  • Maksimum finansal exposure
  • İşlem başına ve dönemsel limit
  • Tespit edilebilirlik
  • Reversal mümkün mü?
  • Kaç account veya tenant etkilenebilir?
  • Otomasyona uygunluk
  • Fraud monitoring ve compensating control
  • Legal ve müşteri etkisi

raporda ayrı business risk context’i olarak yer almalıdır.

17. Secure Code Review kontrol listesi

Business context

  • [ ] Kritik business flow ve owner belirlendi mi?
  • [ ] Happy path dışında abuse ve misuse case’ler tanımlandı mı?
  • [ ] Role, ownership, tenant ve maker-checker kuralları doğrulandı mı?
  • [ ] Limit, amount, quota ve state kuralları test edilebilir invariant olarak yazıldı mı?
  • [ ] Varsayımlar product veya business owner ile doğrulandı mı?

Trust boundary

  • [ ] Fiyat, indirim, komisyon ve limit server-side mı belirleniyor?
  • [ ] Actor identity request payload yerine trusted context’ten mi geliyor?
  • [ ] Tenant ve owner field’ları client tarafından overwrite edilemiyor mu?
  • [ ] Webhook signature dışında local object, amount, currency ve state binding’i yapılıyor mu?
  • [ ] Cache veya client-side state authoritative source olarak kullanılmıyor mu?

Authorization

  • [ ] Function-level ve object-level authorization ayrı ayrı var mı?
  • [ ] Property-level authorization update akışlarında uygulanıyor mu?
  • [ ] Repository query doğru tenant veya owner predicate’ini taşıyor mu?
  • [ ] Maker kendi işlemini onaylayamıyor mu?
  • [ ] Admin, support, delegation ve impersonation flow’ları ayrıca incelendi mi?

Workflow

  • [ ] State transition’lar açıkça tanımlı mı?
  • [ ] Request’ten gelen status doğrudan entity’ye yazılmıyor mu?
  • [ ] Step skip, repeat, reorder ve rollback yolları reddediliyor mu?
  • [ ] Terminal state’lerden uygunsuz dönüş engelleniyor mu?
  • [ ] Bütün endpoint, consumer, job ve admin action’ları aynı invariant’ı enforce ediyor mu?

Concurrency ve replay

  • [ ] Check-then-act pattern’leri incelendi mi?
  • [ ] Tek kullanımlık işlemler unique constraint veya atomic control ile korunuyor mu?
  • [ ] Idempotency key actor, operation ve request hash ile bağlı mı?
  • [ ] Duplicate ve out-of-order event’ler güvenli işleniyor mu?
  • [ ] Retry duplicate external side effect üretmiyor mu?
  • [ ] Concurrency test yalnızca final counter’ı değil, business result sayısını da doğruluyor mu?

Transaction ve dağıtık state

  • [ ] Local transaction’ın kapsamadığı remote side effect’ler belirlendi mi?
  • [ ] Outbox ile domain change aynı transaction’da mı?
  • [ ] Consumer idempotent mi?
  • [ ] Compensation flow ayrı bir business flow olarak korunuyor mu?
  • [ ] Timeout, crash ve partial success için reconciliation var mı?

Anti-automation

  • [ ] Sensitive flow’lar actor, object ve global limitlerle korunuyor mu?
  • [ ] Limit channel’lar arasında ortak mı?
  • [ ] Cache veya rate-limit service arızasında fail-open davranış değerlendirildi mi?
  • [ ] Bot mitigation yalnızca IP veya CAPTCHA’ya dayanmıyor mu?
  • [ ] Abuse sinyalleri audit ve detection üretimine bağlı mı?

Kalıcı remediation

  • [ ] Finding root cause’a göre doğru CWE ile eşlendi mi?
  • [ ] Düzeltme tek endpoint yerine ortak enforcement point’e uygulandı mı?
  • [ ] Negative, state, replay ve concurrency regression test’leri eklendi mi?
  • [ ] Aynı pattern repository genelinde arandı mı?
  • [ ] Retest production’a gidecek aynı build veya commit üzerinde yapıldı mı?

19. Sonuç: Business logic review, kodun ne yaptığından önce ne yapmaması gerektiğini sorar

Business logic açıklarının zor olmasının nedeni gizli bir syntax kullanmaları değildir. Çoğu zaman normal endpoint, geçerli JSON, doğru data type ve izin verilen function kullanılır. Açığı oluşturan şey uygulamanın güvenmemesi gereken veriye güvenmesi, state’i yanlış sırada değiştirmesi, aynı hakkı iki kez kullandırması veya bir actor’ın yapmaması gereken işlemi meşru role altında gerçekleştirmesidir.

Bu nedenle güçlü kod incelemesi:

  1. 1Kritik business objective’i seçer.
  2. 2Actor, object, tenant ve trust boundary ilişkisini çıkarır.
  3. 3Beklenen davranışı test edilebilir invariant’lara dönüştürür.
  4. 4Bütün HTTP ve HTTP dışı entry point’leri haritalar.
  5. 5Server-authoritative ve user-controlled veriyi ayırır.
  6. 6State transition ve approval kurallarını doğrular.
  7. 7Transaction, retry ve partial failure davranışını inceler.
  8. 8Concurrent ve replay execution altında invariant’ı test eder.
  9. 9Finding’i gerçek root cause’a göre sınıflandırır.
  10. 10Remediation’ı constraint, domain control ve regression test ile kalıcılaştırır.

Otomatik SAST bu çalışmanın önemli bir parçasıdır. Riskli mapper, check-then-act, ownership’siz repository call ve client-controlled state pattern’lerini geniş codebase üzerinde arayabilir. Ancak doğru business sonucunu tek başına tanımlayamaz.

Uzman Secure Code Review burada devreye girer. Uygulamanın hangi state’e neden geçebildiğini, hangi actor’ın hangi object üzerinde karar verebildiğini ve birden fazla güvenli görünen component’in birlikte nasıl abuse path oluşturduğunu inceler.

SECNODEX’in Kaynak Kod Analizi ve Secure Code Review hizmeti, otomatik SAST coverage’ını uzman manuel inceleme, business logic ve authorization analizi, root cause odaklı raporlama ve retest ile birleştirir.

Kodda görülen risklerin deployed application üzerindeki gerçek exploitability’sini ve impact’ini doğrulamak için Sızma Testi hizmetimizi inceleyebilirsiniz.

Uygulamanızın kritik flow’ları için doğru review scope’unu belirlemek veya mevcut SAST sonuçlarınızın business risk’e dönüşüp dönüşmediğini değerlendirmek için SECNODEX ile iletişime geçebilirsiniz.

Kaynaklar ve ileri okuma

<!--

EDİTORYAL INTERNAL LINK NOTU

Bu içerik Kaynak Kod Analizi pillar'ının business logic cluster yazısıdır.

Ana pillar:

  • /blog/kaynak-kod-analizi-nedir-kapsamli-rehber

Destek içerikleri:

  • /blog/sast-nedir-hangi-aciklari-bulur-hangilerini-kacirir
  • /blog/secure-code-review-ile-otomatik-sast-taramasi-farki
  • /blog/kaynak-kod-analizi-ile-sizma-testi-farki
  • /blog/java-spring-guvenlik-kod-incelemesi
  • /blog/idor-acigi-neden-hala-bu-kadar-yaygin

Bu cluster yazısından pillar'a erken bölümde doğal bir bağlantı kurulmalıdır.

Pillar içindeki "Hangi zafiyetler kod düzeyinde yakalanır?" veya "Manuel Secure Code Review" bölümünden bu yazıya geri link verilmelidir.

Ana hizmet bağlantısı /hizmetler/kaynak-kod-analizi olmalıdır.

FAQPage schema yalnızca sayfada görünen 12 soru-cevapla birebir eşleşmelidir.

-->

#Business Logic#Secure Code Review#Kaynak Kod Analizi#Authorization#Race Condition#Idempotency#OWASP ASVS#Java Spring Security

Sık sorulan sorular

Business logic açığı nedir?

Business logic açığı, uygulamanın kendi function’larının beklenmeyen sıra, actor, değer, sıklık veya timing ile kullanılması sonucunda business rule’a aykırı bir sonuca izin vermesidir. Request teknik olarak geçerli ve kullanıcı authenticated olabilir.

Business logic açığı ile IDOR aynı şey midir?

Hayır. IDOR, object-level authorization eksikliğidir ve business logic kategorisindeki problemlerin bir örneği olabilir. Business logic ayrıca workflow bypass, price manipulation, race condition, idempotency, approval ve anti-automation hatalarını da kapsar.

SAST business logic açıklarını bulabilir mi?

Bazı pattern’leri ve data flow’ları işaretleyebilir. Fakat uygulamaya özgü doğru fiyatı, izinli state sırasını veya maker-checker kuralını context olmadan her zaman bilemez. Otomasyon hypothesis ve coverage sağlar, uzman Secure Code Review domain doğrulamasını yapar.

Business logic review için kaynak kod tek başına yeterli midir?

Genellikle hayır. Requirement, state diagram, role matrix, pricing ve limit kuralları, event schema ve integration davranışı gerekir. Dokümantasyon eksikse business owner ve developer ile invariant’lar çıkarılmalıdır.

Business invariant nedir?

İşlem boyunca bozulmaması gereken test edilebilir kuraldır. “Refund toplamı captured amount’ı aşamaz” veya “işlemi oluşturan kullanıcı aynı işlemin final approver’ı olamaz” birer invariant örneğidir.

`@Transactional` race condition’ı engeller mi?

Her zaman engellemez. İki transaction aynı state’i okuyup aynı check’i geçebilir. Isolation level, query ve update biçimine göre atomic conditional update, optimistic locking, pessimistic locking veya başka concurrency control gerekebilir.

Idempotency key duplicate işlemi tamamen önler mi?

Doğru tasarlanırsa aynı operation’ın tekrarında tek business result üretmeye yardımcı olur. Ancak key’in actor ve request ile bağlanması, unique constraint, stable result, downstream idempotency ve retry davranışı birlikte tasarlanmalıdır.

Rate limiting business logic açığını kapatır mı?

Tek başına çoğu zaman kapatmaz. IP limit’i dağıtık kaynaklarla aşılabilir. Sensitive flow actor, account, device, business object, payment instrument ve global inventory gibi context’lerle korunmalıdır.

Workflow açığı kodda nasıl anlaşılır?

Request’ten status alınması, generic setStatus, step-specific guard bulunmaması, aynı domain action’a farklı entry point’lerden farklı kontrolle ulaşılması ve terminal state’ten geri dönüşler önemli sinyallerdir. State transition matrix çıkarılarak bütün yasaklı geçişler test edilmelidir.

Business logic finding’i hangi CWE ile raporlanmalıdır?

CWE-840 bir category’dir ve doğrudan vulnerability mapping’i için uygun değildir. Root cause’a göre CWE-841, CWE-639, CWE-837, CWE-362, CWE-367 veya başka somut weakness seçilmelidir.

Business logic açıkları pentest ile mi, kod incelemesiyle mi daha iyi bulunur?

İki yöntem farklı kanıt üretir. Pentest deployed system’de gerçek misuse ve impact’i gösterir. Kod incelemesi bütün caller’ları, transaction boundary’yi, concurrency pattern’ini ve root cause yayılımını görür. Kritik flow’larda birlikte kullanıldıklarında daha yüksek güvence sağlarlar. Yöntemlerin kanıt ve kör nokta farklarını Kaynak Kod Analizi ile Sızma Testi Nerede Ayrışır? yazısında ayrıntılı olarak inceleyebilirsiniz.

Business logic review ne sıklıkla yapılmalıdır?

Ödeme, refund, authorization, tenant, approval, quota, subscription ve identity recovery gibi kritik flow değiştiğinde review yapılmalıdır. Büyük release, yeni integration ve incident sonrasında da risk bazlı tekrar gerekir. SAST her pull request’te çalışabilir, manuel review kritik değişikliklere odaklanabilir.

Siber güvenlik çalışmanızı
SECNODEX ile planlayın

İhtiyacınız tek bir uygulamanın testinden kurum genelinde bir Red Team çalışmasına kadar uzanabilir. Ekibimiz scope’u, kritik varlıkları ve beklenen çıktıları sizinle netleştirir. Ardından iş hedefinize ve risk önceliklerinize uygun, sınırları belirlenmiş bir çalışma planı sunar.