İçeriğe geç

Dinamik Sorgu — Filtre Ekranı Bir Kapıya Dönüşmesin

Orta 9 dk Sık karşılaşılır

Önce şunu oku: join fetch ve Sayfalama — 10 Sipariş İçin Bütün Tablo

30 saniyede özet

Müşteri filtrelerin istediğini doldurur, istediğini boş bırakır. Sorguyu metin birleştirerek kurmak SQL injection kapısı açar, her kombinasyona bir metot yazmak sayıyı katlar. Çözüm: filtre başına bir Specification parçası.

Bir sandviççide sipariş fişi dolduruyorsun: peynir olsun, domates olmasın, bir de not. Fiş kutulu olduğu sürece sorun yok. Ama sipariş tek bir serbest cümle olsaydı, biri sonuna “ve kasayı da ver” yazabilirdi.

  1. Bayt: Hareketler sayfasına filtre ekledim: kategori, tutar, tarih, açıklama. Sorguyu if'lerle kurup birleştiriyorum.

  2. Sen: Bir müşteri açıklama kutusuna tırnaklı bir şey yazmış ve bütün bankanın hareketlerini görmüş.

  3. Bayt: Ama sorguda account_id koşulu vardı!

  4. Bayt: Vardı. Sonra müşterinin yazdığı metin sorgunun bir parçası oldu ve o koşulu etkisiz bıraktı.

Metin birleştirmek neden tehlikeli?

Filtre ekranında her filtre isteğe bağlıdır; sorgu kullanıcının doldurduğuna göre değişir. En kolay görünen yol, dolu filtreleri " AND description LIKE '%" + text + "%'" gibi metne eklemektir.

Müşteri kutuya x' OR 1=1 -- yazarsa tırnak kapanır, OR 1=1 eklenir ve sorgunun geri kalanı yorum olur. Buna SQL injectionKullanıcının yazdığı metnin sorgu metnine karışıp sorgunun yapısını değiştirmesi. Değer bağlı parametre olarak gönderilince olamaz.Sözlükte gör → denir; hesap koşulu artık hiçbir şeyi sınırlamaz.

Kafam karıştı, daha basit anlat

Müşterinin yazdığı metni sorgu metnine yapıştırırsan, sorguyu müşteri de değiştirebilir.

Hızlı kontrolBaşlangıç

SQL injection nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sorguya " AND description LIKE '%" + text + "%'" eklendi. Müşteri x' OR 1=1 -- yazdı. Ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kutulu fiş: filtre başına bir parça

Güvenli olsun diye her filtre kombinasyonu için ayrı bir repository metodu yazdık. Dört isteğe bağlı filtre var, ürün ekibi beşincisini istedi. Kaç metot olur? Cevabı göster

31. n isteğe bağlı filtrenin boş olmayan 2^n - 1 kombinasyonu vardır: dört filtre için 15, beş için 31. Güvenli, ama her yeni filtre sayıyı ikiye katlar.

Kutudaki yazı sadece yazıdır.
Adım adım oku
  1. Sipariş tek bir serbest cümle: sonuna kasayı da ver yazılabilir.
  2. Her kombinasyona ayrı menü: bitmez.
  3. Malzeme başına bir kutu, işaretli olan sayılır.
  4. Kutudaki yazı sadece yazıdır.

Değer sorgu metnine değil, bir bağlı parametreSorgu metninde yalnızca bir yer tutucu (?) bulunur, değer ayrıca gönderilir. Veritabanı değeri hiçbir zaman sorgunun parçası olarak okumaz.Sözlükte gör → gider: sorguda yalnızca ? vardır, değer ayrıca yollanır. Veritabanı onu hiçbir zaman sorgunun parçası olarak okumaz.

Bir SpecificationSpring Data JPA'da tek bir filtre koşulunu anlatan küçük bir parça. Dolu filtrelerin parçaları çalışırken birleştirilir; Criteria API değerleri parametre olarak gönderir.Sözlükte gör →, tek bir filtreyi anlatan küçük bir parçadır. Dolu filtrelerin parçaları çalışırken birleştirilir; beş filtre beş parça demektir.

Kafam karıştı, daha basit anlat

Değer parametre olarak gider. Her filtre bir parça, yalnızca dolu olanlar birleştirilir.

Hızlı kontrolOrta

Bağlı parametre (bind parameter) neden enjeksiyonu engeller?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Specification ile isteğe bağlı filtreler nasıl kurulur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Filtre ekranı: üç arama

Tohum 851951

Oturum: TR01 · bakılacak repository metodu: 1

  1. 🔎 Kategori: MARKET
  2. 🔎 En az 1000 TL, 1 Ekim’den beri
  3. 🔎 Açıklama: x' OR 1=1 --
Hız
Adım 0

Şu an ne oldu?

Metni birleştirerek sorgu kur

4 isteğe bağlı filtre; müşteri istediğini doldurur, istediğini boş bırakır.

Görevler0/3

  • Bir müşteri bütün müşterilerin hareketlerini görsünaçık

    İpucu

    Değeri sorgu metnine yapıştır.

  • Sızıntı olmasın, ama bakılacak metot sayısı 30’u geçsinaçık

    İpucu

    Beşinci filtreyi ekle ve her kombinasyona bir metot yaz.

  • Beş filtrenin hepsi olsun, sızıntı olmasın, metot sayısı beşi geçmesinaçık

    İpucu

    Filtre başına bir parça.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Üçüncü aramadaki tırnak, hesap koşulunu etkisiz bıraktı.
  2. “Her kombinasyona bir metot” seç. Sızıntı yok; metot sayısı 15.
  3. Beşinci filtreyi aç. Metot sayısı 31 oldu.
  4. “Specification” seç. Beş filtre beş parça; üçüncü aramada x' OR 1=1 -- yalnızca aranan bir metin.
Hızlı kontrolOrta

Dört isteğe bağlı filtre için türetilmiş metotlar (findByAccountIdAndCategory…) yazıyoruz. Beşinci filtre gelince metot sayısı ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Filtre ekranında hesap koşulu nereden gelmeli?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

JPQL’i güvenli sanmak. "... WHERE d.description LIKE '%" + text + "%'" JPQL’de de enjeksiyona açıktır. Parametre kullan.

Hesap filtresini istekten almak. Filtre ekranı “hangi hesap” diye sormamalı; o değer her zaman oturumdan gelir ve her sorguya eklenir.

Sıralama sütununu kullanıcıdan almak. ORDER BY parametre olamaz; kullanıcının seçtiği sütunu izinli bir listeyle eşleştir.

Kafam karıştı, daha basit anlat

JPQL’de de parametre kullan, hesabı oturumdan al, sıralama sütununu izinli listeden seç.

Hızlı kontrolOrta

Kullanıcı sıralama sütununu seçebiliyor. Bunu nasıl güvenli yaparsın?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
Derinleş · Hareket filtresi: beş isteğe bağlı filtre, tek güvenli sorgu 4 dosya · ~88 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/history/ TransactionFilter.java Filtre: her alan boş olabilir; ekran ne doldurduysa o kadar.

src/main/java/com/bank/history/TransactionFilter.java
// Every field is optional: the screen sends only what the customer filled in.
record TransactionFilter(
Category category,
BigDecimal minAmount,
LocalDate from,
String text,
String counterpartyIban) {
}

src/main/java/com/bank/history/ TransactionSpecs.java Specification'lar: hesap her zaman oturumdan, diğer filtreler yalnızca doluysa; değerler bağlı parametre.

src/main/java/com/bank/history/TransactionSpecs.java
final class TransactionSpecs {
private static final ZoneId ISTANBUL = ZoneId.of("Europe/Istanbul");
private TransactionSpecs() {
}
static Specification<AccountTransaction> matching(String sessionAccount, TransactionFilter f) {
List<Specification<AccountTransaction>> parts = new ArrayList<>();
// Always present, always from the session: never a filter the customer can change.
parts.add((root, query, cb) -> cb.equal(root.get("accountId"), sessionAccount));
if (f.category() != null) {
parts.add((root, query, cb) -> cb.equal(root.get("category"), f.category()));
}
if (f.minAmount() != null) {
parts.add((root, query, cb) -> cb.greaterThanOrEqualTo(root.<BigDecimal>get("amount"), f.minAmount()));
}
if (f.from() != null) {
Instant start = f.from().atStartOfDay(ISTANBUL).toInstant();
parts.add((root, query, cb) -> cb.greaterThanOrEqualTo(root.<Instant>get("createdAt"), start));
}
if (f.text() != null && !f.text().isBlank()) {
String pattern = "%" + f.text().toLowerCase(Locale.ROOT) + "%"; // bound as a parameter, never spliced
parts.add((root, query, cb) -> cb.like(cb.lower(root.get("description")), pattern));
}
if (f.counterpartyIban() != null) {
parts.add((root, query, cb) -> cb.equal(root.get("counterpartyIban"), f.counterpartyIban()));
}
return Specification.allOf(parts);
}
}

src/main/java/com/bank/history/ TransactionHistory.java Servis: repository tek bir findAll çağrısı alıyor, sıralama izinli bir listeden.

src/main/java/com/bank/history/TransactionHistory.java
interface TransactionRepository extends JpaRepository<AccountTransaction, Long>, JpaSpecificationExecutor<AccountTransaction> {
}
@Service
class TransactionHistory {
private static final Map<String, String> SORTABLE = Map.of("tarih", "createdAt", "tutar", "amount");
private final TransactionRepository transactions;
TransactionHistory(TransactionRepository transactions) {
this.transactions = transactions;
}
Page<AccountTransaction> search(String sessionAccount, TransactionFilter filter, String sortKey, int page) {
String column = SORTABLE.getOrDefault(sortKey, "createdAt"); // ORDER BY cannot be a parameter: allowlist it
Pageable pageable = PageRequest.of(page, 20, Sort.by(Sort.Direction.DESC, column));
return transactions.findAll(TransactionSpecs.matching(sessionAccount, filter), pageable);
}
}

src/test/java/com/bank/history/ TransactionSpecsIT.java Test: saldırı metni başka hesabın hareketini getirmiyor, filtreler birleşiyor; gerçek bir PostgreSQL ile.

src/test/java/com/bank/history/TransactionSpecsIT.java
@DataJpaTest
@Testcontainers
class TransactionSpecsIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired TransactionRepository transactions;
@BeforeEach
void seed() {
transactions.saveAll(List.of(
AccountTransaction.of("TR01", Category.MARKET, "245.90", "Migros"),
AccountTransaction.of("TR02", Category.KIRA, "18000.00", "Ekim kirası")));
}
@Test
void anInjectionPayloadIsJustText() {
var filter = new TransactionFilter(null, null, null, "x' OR 1=1 --", null);
assertThat(transactions.findAll(TransactionSpecs.matching("TR01", filter))).isEmpty();
}
@Test
void filledFiltersCombineAndStayInsideTheAccount() {
var filter = new TransactionFilter(Category.MARKET, new BigDecimal("100"), null, null, null);
assertThat(transactions.findAll(TransactionSpecs.matching("TR01", filter)))
.extracting(AccountTransaction::getDescription).containsExactly("Migros");
}
}

Kendini sına

Şimşek turu1/4

Sorgu JPQL ile yazılmışsa metin birleştirmek güvenlidir.

Soru 1/3İleri

Sorgu JPQL ile metin birleştirerek yazıldı. Güvenli mi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Kullanıcının yazdığı değeri sorgu metnine yapıştırmak, ona sorguyu değiştirme izni vermektir. Değer her zaman bağlı parametre olarak gider.
  2. 2 İsimden türetilen repository metotları sabit aramalar için iyidir. İsteğe bağlı n filtre için 2^n - 1 kombinasyon gerekir; yeni bir filtre sayıyı ikiye katlar.
  3. 3 Specification ile her filtre küçük bir parçadır; yalnızca dolu olanlar çalışırken birleştirilir. Hesap filtresi müşteriden değil, her zaman oturumdan gelir.
Sonraki kapı Kodda yalnızca yeni bir kart durumu ekledin. Ertesi sabah bloklu kartlar dondurulmuş görünüyor. Veritabanına kimse dokunmadı. Ne oldu? Enum Eşleme — Bloklu Kart Neden Bir Gün Dondurulmuş Göründü? · 8 dk

4 kart sonraki derste seni bekliyor

0/4 kart bu dersten toplandı

Bu dersin üstüne kurulanlar

Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.