Vibe Coderin El Kitabı · Bölüm 5.3 / 15

Vibe Coder #5.3: Güvenlik Denetimi – AI Kodunda Gizli Mayınları Avlamak

Üç mutlu yol testi yeşil, sınıfta üç mayın: SQL'e gömülü girdi, eval() ve kodda şifre. Ajanın yazdığı koda OWASP gözüyle bakıyoruz; kalıp arayan bir PHP kapısı, Semgrep ile iz sürme ve dört ekosistemin model denetçileri neyi bulup neyi kaçırıyor.

16 dk okuma Vibe Coding

Bu yazının İngilizcesi de var →
Şafakta bir çayırda turuncu işaret bayrakları, ön planda katlanmış elle çizilmiş haritanın üzerinde duran metal dedektörü ve toprağın içinden yarı görünen yuvarlak metal disk; AI kod güvenliği yazısının mayın tarlası metaforu.

Bir önceki bölümü şu cümleyle kapatmıştık: testler yeşil, mühür sağlam, ama yeşil güvenli demek değil; SQL injection'a açık bir sorgu da kendi testini mutlulukla geçer. Bu bölümde o cümleyi kanıtlayarak başlıyoruz. Konu AI kod güvenliği: ajanın yazdığı koda OWASP gözüyle bakmak, içindeki mayınları bulmak ve bir dahaki sefere kapıda yakalamak.

Geçen ay PHP2Shell yazısında yangın PHP'nin kendisindeydi; elimizden gelen kapıyı kapatıp yamayı beklemekti. Bu bölümdeki mayınlar öyle değil. PHP sağlam, sunucu güncel; mayını bizim deponuza, bizim onayımızla giren kod döşüyor. Kimse kötü niyetle gömmedi. Ajan en kısa yoldan gitti, en kısa yol da mayın tarlasının ortasından geçiyordu.

Metaforumuz bu: mayın tarlası. Elimizde iki alet olacak. Biri metal dedektörü: hızlı, çok öter, plastik mayını görmez. Öbürü iz sürücü: girdinin nereden girip nereye aktığını izler, ama ancak bildiği yolda. Bölümün sonunda ikisinin de neyi bulup neyi kaçırdığını sayılarla göreceksiniz.

AI kod güvenliği neden ayrı bir denetim ister?#

Çünkü ajanın yazdığı kod çalışıyor. Sorun da orada. Çalışmayan kodu herkes fark eder; çalışan ama açık bırakan kodu ancak bakan fark eder.

Veracode'un 2025 GenAI Code Security raporu (30.07.2025) 100'den fazla dil modeline 80 kodlama görevi veriyor. Sonuç: vakaların %45'inde güvenlik açığı. Java'da başarısızlık oranı %70'in üstünde; Python, C# ve JavaScript'te %38 ile %45 arasında. Siteler arası betik (XSS, CWE-80) savunmasında modeller vakaların %86'sında, log injection'da (CWE-117) %88'inde başarısız. PHP raporda yok; bizim dilimiz için sayı veremem, ama birazdan PHP'de kendi küçük ölçümümüzü yapacağız.

Hangi gözle bakacağımızı da OWASP Top 10:2025 söylüyor. Listenin sekizinci sürümünde A05 Injection iki sıra düşüp beşinciliğe inmiş, ama hâlâ en çok test edilen kategorilerden biri ve altında 38 CWE var. Bu bölümün üç mayını üç ayrı kategoriye düşüyor: SQL'e gömülü girdi ve eval() A05'e, koddaki şifre A04 Cryptographic Failures ile A07 Authentication Failures'ın kesiştiği yere, olmayan paketi kurmak da listeye bu sürümde giren A03 Software Supply Chain Failures'a.

Üç mayın, üç yeşil test#

Senaryo: ürün arama ve kampanya indirimi. Ajana "ürünleri ada göre ara, panelden gelen kampanya formülünü (fiyat * 0.9 gibi) fiyata uygula" dendiğini düşünün. Aşağıdaki sınıfı ajan değil ben yazdım; ama ajan çıktısında sık gördüğümüz üç kalıbı bilerek içine koydum. Ölçmek istediğim şey ajan değil, testin ve denetimin neyi görüp neyi görmediği.

PHP
<?php
​
declare(strict_types=1);
​
final class UrunArama
{
    public function __construct(private PDO $db)
    {
    }
​
    public static function baglan(): self
    {
        return new self(new PDO('mysql:host=127.0.0.1;dbname=magaza', 'magaza', 'Magaza-2026!')); // Mayın 1: kodda şifre
    }
​
    /**
     * Ürünleri ada göre arar, kampanya formülünü fiyata uygular ("fiyat * 0.9" gibi).
     *
     * @return list<array{ad: string, fiyat: float}>
     */
    public function ara(string $kelime, string $formul = 'fiyat'): array
    {
        $sql = "SELECT ad, fiyat FROM urunler WHERE ad LIKE '%$kelime%' ORDER BY ad"; // Mayın 2: SQL'e gömülü girdi
        $ifade = str_replace('fiyat', '$fiyat', $formul);
        $sonuc = [];
        foreach ($this->db->query($sql)->fetchAll(PDO::FETCH_ASSOC) as $satir) {
            $fiyat = (float) $satir['fiyat'];
            $sonuc[] = ['ad' => (string) $satir['ad'], 'fiyat' => (float) eval("return $ifade;")]; // Mayın 3: eval
        }
​
        return $sonuc;
    }
}

Testleri de mutlu yoldan yazdım: ada göre bulur, formülü uygular, bulamayınca boş döner. SQLite bellek veritabanıyla PHPUnit 9.6.17'de çalıştırdım (08.11.2026, PHP 8.3.6 CLI):

Bash
$ phpunit
OK (3 tests, 3 assertions)

Üç test, üç yeşil, üç mayın. Şimdi aynı sınıfa kötü niyetli girdiyle soralım:

Bash
$ php deneme.php
1) Normal: [{"ad":"Kırmızı Kupa","fiyat":108},{"ad":"Mavi Kupa","fiyat":90}]
2) SQL: [{"ad":"yonetici@ornek.test","fiyat":0}]
3) eval: [{"ad":"Defter","fiyat":41}]

İkinci satırda arama kutusuna x' UNION SELECT eposta, sifre_ozeti FROM kullanicilar -- yazdım; ürün listesinde yöneticinin e-posta adresi çıktı. Üçüncü satırda formül yerine strlen(php_uname()) gönderdim; sunucunun işletim sistemi bilgisini okuyan fonksiyon çalıştı, dönen metnin uzunluğu fiyat diye listelendi. strlen yerine başka bir fonksiyon da yazılabilirdi. Ben yazmadım.

Acı Gerçek: testler yanlış değil. Testler ne istediğimizi soruyordu, neyi istemediğimizi değil. Dilek "ürünleri bul" idi; "ve başka hiçbir şeyi bulma" dilekte yoktu.

Mayın 1: Kodda şifre#

En sessiz mayın bu, çünkü hiçbir test onu çalıştırmıyor. baglan() ilk commit'te depoya giriyor, depo bir gün başka bir ekibe, bir yükleniciye ya da yanlışlıkla herkese açılıyor. GitGuardian'ın 2025 raporuna göre 2024'te herkese açık GitHub depolarında 23,8 milyon sır sızmış; 2022'de sızanların %70'i rapor yazıldığında hâlâ geçerliymiş. Commit'ten silmek yetmiyor, git geçmişi unutmuyor. Sızan şifre değiştirilir, silinmez.

Teknik Detay: ajan bunu kötülükten yapmıyor. Ona "bağlantıyı kur" dediniz, çalışan en kısa bağlantı şifreyi içeren satır. .env dosyasını okumak, eksikse anlamlı hata vermek ekstra iş.

Mayın 2: SQL'e gömülü girdi#

Klasik ama ölmüyor. "... LIKE '%$kelime%'" satırında girdi verinin değil, komutun parçası oluyor. Çaresi de yıllardır aynı: hazırlanmış ifade (prepared statement) ve parametre. Burada bir ince nokta daha var: LIKE için parametre kullansanız bile % ve _ joker karakter olarak kalır. Kullanıcı % aradığında bütün tabloyu getirmek injection değil, ama istemediğiniz bir davranış.

Mayın 3: eval() cazibesi#

"Panelden formül gelsin" isteği ajanı doğrudan eval()'e götürüyor, çünkü formülü çalıştırmanın en kısa yolu formülü kod gibi çalıştırmak. PHP kılavuzu bu konuda net: eval() "very dangerous", çünkü rastgele PHP kodu çalıştırmaya izin veriyor; kullanımı önerilmiyor ve kullanıcıdan gelen veri doğrulanmadan içine verilmemeli. Aynı aileden unserialize() için de kılavuzun uyarısı şu: güvenilmeyen girdiyi, allowed_classes ne olursa olsun, ona vermeyin; JSON kullanın.

Bonus mayın: olmayan paket#

Dördüncüsü kodda değil, composer require satırında. Spracklen ve arkadaşlarının paket halüsinasyonu çalışması 576.000 kod örneğinde ticari modellerde ortalama en az %5,2, açık modellerde %21,7 oranında var olmayan paket adı buluyor; toplam 205.474 farklı uydurma ad. Saldırganın işi o adı alıp paketi yayımlamak. composer audit bilinen güvenlik danışmalarına bakar; dün yayımlanmış kötü niyetli bir paketi tanımaz. Ajan yeni bir paket önerdiğinde Packagist sayfasını, bakımcıyı ve paketin yaşını siz açıp bakın.

Mayınları temizlemek#

Temiz sürüm üç şey yapıyor: şifreyi ortamdan okuyor, sorguyu parametreli kuruyor ve formülü koddan veriye çeviriyor. Panelden gelen şey artık çalıştırılacak bir ifade değil, bir indirim türü ve bir sayı.

PHP
<?php
​
declare(strict_types=1);
​
enum Indirim: string
{
    case Yok = 'yok';
    case Yuzde = 'yuzde';
    case Tutar = 'tutar';
}
​
final class UrunArama
{
    public function __construct(private PDO $db)
    {
    }
​
    public static function baglan(): self
    {
        $dsn = getenv('DB_DSN');
        if ($dsn === false || $dsn === '') {
            throw new RuntimeException('DB_DSN tanımlı değil.');
        }
​
        return new self(new PDO($dsn, getenv('DB_KULLANICI') ?: null, getenv('DB_SIFRE') ?: null));
    }
​
    /** @return list<array{ad: string, fiyat: float}> */
    public function ara(string $kelime, Indirim $indirim = Indirim::Yok, float $deger = 0.0): array
    {
        $kalip = '%' . addcslashes($kelime, '%_\\') . '%';
        $sorgu = $this->db->prepare("SELECT ad, fiyat FROM urunler WHERE ad LIKE ? ESCAPE '\\' ORDER BY ad");
        $sorgu->execute([$kalip]);
​
        $sonuc = [];
        foreach ($sorgu->fetchAll(PDO::FETCH_ASSOC) as $satir) {
            $fiyat = (float) $satir['fiyat'];
            $sonuc[] = ['ad' => (string) $satir['ad'], 'fiyat' => match ($indirim) {
                Indirim::Yok => $fiyat,
                Indirim::Yuzde => round($fiyat * (1 - min(max($deger, 0.0), 90.0) / 100), 2),
                Indirim::Tutar => max(round($fiyat - $deger, 2), 0.0),
            }];
        }
​
        return $sonuc;
    }
}

Gerçek: "formülü veri yap" kuralı esneklikten bir şey alıyor. Panel artık fiyat * 0.9 + 5 yazamıyor. Gerçekten serbest formül gerekiyorsa çözüm eval() değil, yalnızca sayı, fiyat ve dört işlem tanıyan küçük bir ayrıştırıcı. Çoğu kampanya ise üç türe sığıyor; önce onu deneyin.

Test sınıfına 5.2'deki mantıkla iki dilek daha ekledim: bu kez neyi istemediğimizi yazan dilekler.

PHP
    /** Kötü niyetli dilek: girdi sorgunun parçası olmamalı. */
    public function testSqlGirdisiVeriOlarakKalir(): void
    {
        self::assertSame([], $this->arama->ara("x' UNION SELECT eposta, sifre_ozeti FROM kullanicilar --"));
    }
​
    public function testJokerKarakterlerDuzMetindir(): void
    {
        self::assertSame(['%50 Kupon'], array_column($this->arama->ara('%'), 'ad'));
    }

Temiz sürümde OK (5 tests, 5 assertions). Aynı iki testi mayınlı sürüme karşı çalıştırdığımda ikisi de kırmızı: Tests: 2, Assertions: 2, Failures: 2. Güvenlik testi de bir dilek. Yazmazsanız cin onu yerine getirmek zorunda değil.

Metal dedektörü: değişen dosyada mayın kalıbı aramak#

Testi her mayın için ayrı yazmak gerekiyor ve hangi mayını unuttuğunuzu bilmiyorsunuz. İkinci savunma hattı, ajanın değiştirdiği her PHP dosyasında bilinen kalıpları arayan bir kapı. Aşağıdaki betik regex ile değil PHP'nin kendi ayrıştırıcısıyla (token_get_all()) çalışıyor; yorumdaki ya da metin içindeki eval kelimesine ötmüyor. Üç şey arıyor: eval() ve komut/nesne çalıştıran fonksiyonlar, argümanında değişken ya da birleştirme olan query()/exec()/mysqli_query() çağrıları ve kodda duran şifreler. Bulursa 2 ile çıkıyor; 4.2'den ve 5.2'den hatırlayacağınız gibi ajan kancaları 2'yi "geri dön, düzelt" diye anlıyor.

PHP
<?php
​
declare(strict_types=1);
​
/**
 * Mayın kapısı: değişen PHP dosyalarında üç mayın türünü arar.
 *   php mayin-kapisi.php [taban]   taban verilmezse HEAD; izlenmeyen yeni dosyalar da taranır
 * Çıkış: 0 temiz, 2 mayın var (ajan kancaları 2'yi "geri dön, düzelt" diye anlar), 1 kullanım hatası.
 * Bu bir kapı, denetçi değil: yalnızca kalıp arar, veri akışını izlemez.
 */
​
const TEHLIKELI_FONKSIYONLAR = [
    'exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen',
    'unserialize', 'create_function', 'assert',
];
const SORGU_METOTLARI = ['query', 'exec', 'mysqli_query', 'mysqli_multi_query'];
const SIR_ADI = '/(pass(word)?|passwd|sifre|parola|secret|token|api_?key|anahtar)/i';
​
/** @return list<string> */
function degisenDosyalar(string $taban): array
{
    $komutlar = [
        'git diff --name-only --diff-filter=ACMR ' . escapeshellarg($taban),
        'git ls-files --others --exclude-standard',
    ];
    $dosyalar = [];
    foreach ($komutlar as $komut) {
        exec($komut . ' 2>/dev/null', $cikti, $kod);
        if ($kod !== 0) {
            fwrite(STDERR, "Git komutu başarısız: $komut\n");
            exit(1);
        }
        $dosyalar = array_merge($dosyalar, $cikti);
        $cikti = [];
    }
​
    return array_values(array_unique(array_filter(
        $dosyalar,
        static fn (string $yol): bool => str_ends_with($yol, '.php') && is_file($yol) && $yol !== 'mayin-kapisi.php'
    )));
}
​
/**
 * Açılan parantezden kapanana kadar argümanları üst düzey virgüllerden böler.
 *
 * @param list<mixed> $jetonlar
 * @return list<list<mixed>>
 */
function argumanlar(array $jetonlar, int $acilis): array
{
    $derinlik = 0;
    $liste = [[]];
    for ($i = $acilis, $n = count($jetonlar); $i < $n; $i++) {
        $j = $jetonlar[$i];
        if ($j === '(' || $j === '[') {
            if ($derinlik++ === 0) {
                continue;
            }
        } elseif ($j === ')' || $j === ']') {
            if (--$derinlik === 0) {
                break;
            }
        } elseif ($j === ',' && $derinlik === 1) {
            $liste[] = [];
            continue;
        }
        if (!is_array($j) || !in_array($j[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
            $liste[count($liste) - 1][] = $j;
        }
    }
​
    return $liste;
}
​
function sonrakiAnlamli(array $jetonlar, int $i): int
{
    while (isset($jetonlar[++$i]) && is_array($jetonlar[$i]) && $jetonlar[$i][0] === T_WHITESPACE) {
    }
​
    return $i;
}
​
/** @return list<string> "satır: açıklama" */
function tara(string $yol): array
{
    $jetonlar = token_get_all((string) file_get_contents($yol));
    $bulgular = [];
    $satir = 1;
    $tersTirnak = false;
    foreach ($jetonlar as $i => $j) {
        if (!is_array($j)) {
            if ($j === '`' && ($tersTirnak = !$tersTirnak)) {
                $bulgular[] = "$satir: ters tırnakla kabuk komutu";
            }
            continue;
        }
        [$tur, $metin, $satir] = $j;
​
        if ($tur === T_EVAL) {
            $bulgular[] = "$satir: eval() — formülü veri yap, kod yapma";
            continue;
        }
        if ($tur !== T_STRING) {
            continue;
        }
        $sonraki = sonrakiAnlamli($jetonlar, $i);
        if (($jetonlar[$sonraki] ?? null) !== '(') {
            continue;
        }
        $isim = strtolower($metin);
        $once = $jetonlar[$i - 1] ?? null;
        $metotMu = is_array($once) && in_array($once[0], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON], true);
        $tanimMi = is_array($once) && $once[0] === T_WHITESPACE
            && is_array($jetonlar[$i - 2] ?? null) && $jetonlar[$i - 2][0] === T_FUNCTION;
​
        if (!$metotMu && !$tanimMi && in_array($isim, TEHLIKELI_FONKSIYONLAR, true)) {
            $bulgular[] = "$satir: $metin() — girdiyle buluşursa komut/nesne çalıştırır";
        }
        if (in_array($isim, SORGU_METOTLARI, true) && !$tanimMi) {
            $ilk = argumanlar($jetonlar, $sonraki)[$isim === 'mysqli_query' || $isim === 'mysqli_multi_query' ? 1 : 0] ?? [];
            foreach ($ilk as $parca) {
                if ($parca === '.' || $parca === '"' || (is_array($parca) && in_array($parca[0], [T_VARIABLE, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true))) {
                    $bulgular[] = "$satir: $metin() değişkenle kurulan sorgu — prepare() ve parametre kullan";
                    break;
                }
            }
        }
        if ($isim === 'pdo' && is_array($once) && ($once[0] === T_WHITESPACE) && ($jetonlar[$i - 2][0] ?? null) === T_NEW) {
            $ucuncu = argumanlar($jetonlar, $sonraki)[2] ?? [];
            if (isset($ucuncu[0]) && is_array($ucuncu[0]) && $ucuncu[0][0] === T_CONSTANT_ENCAPSED_STRING && strlen($ucuncu[0][1]) > 2) {
                $bulgular[] = "$satir: new PDO() şifresi kodda — ortam değişkeninden oku";
            }
        }
    }
​
    // Adı sır gibi duran bir şeye en az sekiz karakterlik sabit metin atanıyor mu?
    foreach (file($yol) ?: [] as $no => $satirMetni) {
        if (preg_match('/([A-Za-z_]\w*)[\'"]?\s*(=>|=)\s*[\'"]([^\'"\s]{8,})[\'"]/', $satirMetni, $m) === 1
            && preg_match(SIR_ADI, $m[1]) === 1) {
            $bulgular[] = ($no + 1) . ": {$m[1]} sabit metin — sırrı koda değil ortama koy";
        }
    }
​
    return $bulgular;
}
​
if (!is_dir('.git')) {
    fwrite(STDERR, "Git deposunun kökünden çalıştırın.\n");
    exit(1);
}
​
$toplam = 0;
foreach (degisenDosyalar($argv[1] ?? 'HEAD') as $yol) {
    foreach (tara($yol) as $bulgu) {
        fwrite(STDERR, "$yol:$bulgu\n");
        $toplam++;
    }
}
​
if ($toplam > 0) {
    fwrite(STDERR, "$toplam mayın. Kapı kapalı.\n");
    exit(2);
}
​
echo "Değişen dosyalarda bilinen mayın kalıbı yok.\n";
exit(0);

Ajanın eklediği dosyalar henüz commit'lenmemişken, iskelet commit'ine karşı çalıştırdım:

Bash
$ php mayin-kapisi.php
src/UrunArama.php:13: new PDO() şifresi kodda — ortam değişkeninden oku
src/UrunArama.php:26: query() değişkenle kurulan sorgu — prepare() ve parametre kullan
src/UrunArama.php:28: eval() — formülü veri yap, kod yapma
3 mayın. Kapı kapalı.
$ echo $?
2

Temiz sürümde Değişen dosyalarda bilinen mayın kalıbı yok., çıkış 0. Ayrıca küçük bir kenar dosyasıyla denedim: birleştirmeyle kurulan exec(), unserialize(), ters tırnak, değişkenli mysqli_query(), sabit metin atanmış ODEME_API_ANAHTARI ve 'password' dizi anahtarı yakalandı. Sabit query('SELECT 1'), prepare()->execute(), json_decode(), getenv('DB_SIFRE') ve şifresiz new PDO('sqlite::memory:') sessiz geçti.

Acı Gerçek: dedektör çok öter. $sql = "SELECT COUNT(*) FROM urunler"; $db->query($sql); tamamen zararsız, ama kapı onu da durduruyor; değişkenin içinde ne olduğunu bilmiyor. Bunu bilerek böyle bıraktım. Ajan için prepare() yazmak bedava, bir mayını kaçırmak değil. Öbür yönde de sınır var: plastik mayını görmez. $kelime'yi önce bir diziye koyup oradan sorguya veren bir kod kalıba uymazsa geçer.

İz sürücü: girdinin yolunu takip etmek#

Dedektör şekle bakıyor. İz sürücü başka bir soru soruyor: dışarıdan gelen veri, temizlenmeden tehlikeli bir yere ulaşıyor mu? Bu taint analizi (kirli veri takibi). Semgrep bunu Community Edition'da kural dosyasıyla yapabiliyor; Semgrep, Inc. geliştiriyor, lisansı LGPL-2.1. Bulut sandbox'a 1.184.0 sürümünü kurup şu kuralı yazdım:

YAML
rules:
  - id: girdi-sorguya-akiyor
    mode: taint
    languages: [php]
    severity: ERROR
    message: Dış girdi SQL sorgusuna ya da eval'e akıyor. prepare() ve parametre kullanın.
    pattern-sources:
      - pattern: $_GET
      - pattern: $_POST
      - pattern: $_REQUEST
      - pattern: $_COOKIE
    pattern-sinks:
      - pattern: $DB->query(...)
      - pattern: $DB->exec(...)
      - pattern: mysqli_query($C, ...)
      - pattern: eval(...)

Üç durumla denedim: $_GET'i aynı fonksiyon içinde sorguya birleştiren bir fonksiyon, aynı veriyi prepare() ile kullanan bir fonksiyon ve $_GET'i parametre olarak alıp başka bir fonksiyonda sorguya gömen bir çağrı. Üstüne mayınlı UrunArama sınıfı.

Bash
$ semgrep --config mayin.yml --metrics=off --no-git-ignore akis.php eski/UrunArama.php
           10┆ return $db->query($sql)->fetchAll();
 • Findings: 1 (1 blocking)

Tek bulgu ve doğru bulgu: aynı fonksiyon içindeki akış. prepare() kullanan fonksiyona ötmedi, güzel. Ama iki mayını kaçırdı. Fonksiyon sınırını geçen akışı görmedi, çünkü Semgrep dokümanına göre fonksiyonlar arası taint analizi Pro özelliği. UrunArama sınıfında ise hiç $_GET yok; girdi sınıfa parametreyle geliyor, iz sürücünün izleyeceği bir kaynak yok.

Sonuç: dedektör sınıftaki üç mayının üçünü buldu ama zararsız satıra da öttü; iz sürücü hiç yanlış ötmedi ama sınıfta hiçbir şey bulmadı. İkisi aynı sorunun iki aracı değil, iki ayrı sorunun cevabı. Kapıya ikisini birden koyun.

Dört ekosistemde güvenlik denetçisi#

Model tabanlı denetçiler de artık her ekosistemde var. Kalıp aramıyorlar, kodu okuyorlar; bu yüzden yukarıdaki iki aletin göremediği şeyleri (yetki atlama, yanlış nesneye erişim) yakalayabiliyorlar. Dokümanlar 08.11.2026'da okundu, hiçbirini bu yazı için çalıştırmadım.

Araç Ne yapıyor Kilit mi, göz mü?
Claude Code /security-review Dalınızla origin'in varsayılan dalı arasındaki farkı injection, yetki ve veri sızıntısı açısından inceler (komutlar) Göz; istediğinizde çalışır
Claude Code security-guidance eklentisi Her düzenlemede eval(, pickle, .innerHTML = gibi kalıpları arar, tur sonunda ayrı bir model farkı inceler, commit'te daha derin bakar (doküman) Göz; dokümanın kendi cümlesiyle hiçbir katman yazmayı ya da commit'i engellemez
OpenAI Codex Security Eski adı Aardvark; depoya özel tehdit modeli çıkarır, bulguları mümkünse sandbox'ta doğrular, yama önerir (duyuru) Göz; bulut sürümü araştırma önizlemesinde
Gemini CLI security eklentisi /security:analyze yerel git diff'i sabit sırlar, injection ve yetki açısından inceler; /security:scan-deps bağımlılıkları OSV-Scanner ile tarar (depo, Apache-2.0) Göz
Semgrep CE, composer audit Kural tabanlı tarama, bilinen danışmalar Kapı olabilir; CI'da çıkış koduyla

Tablonun son sütunu bu bölümün asıl mesajı. Model denetçi ikinci bir göz; bulguyu ajana geri verir, ajan düzeltir. Ama model kaçırabilir ve hiçbiri kapıyı kilitlemiyor. Kilit, çıkış koduyla duran bir komut. 5.2'deki Claude Code Stop kancasına mayın kapısını eklemek tek satır:

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cd \"$CLAUDE_PROJECT_DIR\" && php test-muhru.php kontrol && php mayin-kapisi.php origin/main"
          }
        ]
      }
    ]
  }
}

Aider'da aynı zincir test komutuna giriyor; Codex ve Cursor'da da 5.2'de anlattığımız Stop/stop kancaları aynı komutu çağırabilir:

Bash
aider src/UrunArama.php --read tests/UrunAramaTest.php \
  --test-cmd "php test-muhru.php kontrol && php mayin-kapisi.php origin/main" --auto-test

Ne zaman yetmez?#

Dedektör, iz sürücü ve model denetçi bilinen mayın türlerini arıyor. OWASP listesinin birinci sırasındaki A01 Broken Access Control ise çoğu zaman kalıp değil, eksik bir satır: "bu sipariş bu müşterinin mi?" sorusunu sormayan bir sorgu. Hiçbir tarayıcı sizin iş kuralınızı bilmez. Multi-storefront yazısında her tablo için "bu veri kimin?" diye sormuştuk; o soru burada da elle sorulur. Codex Security'nin tehdit modeli çıkarması da bu yüzden anlamlı: önce tarlanın haritası, sonra dedektör.

Bir de sıralama meselesi var. Bu kapıların hiçbiri ajanın sunucuda çalıştırdığı komutu görmez; onlar koda bakar. Ajanın canlı ortamda neye dokunabileceği izin ayarının işi, Kentsel Dönüşüm #1'de anlattığımız gibi.

Kod tarafında mührü ve mayın kapısını kurduk; ama ikisi de şu an yalnızca ajanın kendi oturumunda çalışıyor. Ajan kapıyı atlarsa, kancayı kapatırsa ya da işi siz elle commit'lerseniz kimse bakmıyor. Serinin bir sonraki bölümü, Vibe Coder #6.1: CI/CD'nin Vibe Hali, bu kapıları ajanın oturumundan çıkarıp her commit'in geçmek zorunda olduğu boru hattına taşıyacak. Serinin tamamı ve yayınlanan bölümlerin listesi Vibe Coder El Kitabı sayfasında.

Dedektör öttüğünde durun. Ötmediğinde de haritaya bakmadan koşmayın. Teknolojiyle kalın.

Yorumlar

0 yorum

Yorum yazın

kişisine yanıt

Yorumlar onaylandıktan sonra görünür. E-posta adresiniz kimseyle paylaşılmaz. Kişisel verileriniz nasıl işlenir?