# Güvenlik Pratikleri

https://dev.wisecp.com/tr/guvenlik-pratikleri

Bu platformun sağladığı güvenlik katmanları ve her birini devreye sokan çağrı. Yanında yayına çıkmış üç hata. Sürekli açık kalan bir kapı, koruduğu değeri yok eden bir filtre, çalıştırılabilir işaretleme olarak sunulan bir yükleme.

## Genel Bakış

Aşağıdaki her katman bu kod tabanında var, adı belli tek bir çağrıyla devreye girer ve adı belli tek bir yerde uygulanır. Hiçbiri kendiliğinden çalışmaz. Bir controller kendi girdisini filtrelemez, açık bir form isteyene kadar korunmaz.

Üçüncü taraf kodu en sık bu iki katmanda yanılır. Girdi filtreleme alan ve tip başına olur. Müşteri API'sinde sahiplik istek başına değil, sorgu başına olur.

## Ön Koşullar

- Filtre sözlüğünün tamamı için [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme). Bu makale yanlış filtreyi seçmenin sonuçlarını anlatır.
- Güvenli hâle getirilecek bir ekran ve ona kimin ulaştığı. Bir admin operation'ı, açık bir form, bir müşteri ucu ve bir modül ucu farklı katmanları devreye sokar.
- Başka bir sekmede açık duran panel Güvenlik ayarları. Aşağıdaki her eşik bir yapılandırma anahtarı. Modülünüze sabit yazılmış bir sınır, operatörün ayarını yok sayar.

## Yapı

### Katmanlar

| Katman | Neyi durdurur | Devreye sokan |
| --- | --- | --- |
| Girdi filtreleme | Bir alanın içine sokuşturulan enjeksiyon ve işaretleme | **Kodunuz**, alan başına tek çağrı, alana uyan tiple |
| Çıktı kodlama | Saklanmış değerler geri gösterilirken çalışır | Admin şablonlarında ve JSON'da **kodunuz**. Website şablonları kendiliğinden kaçırır |
| Yetki kontrolü | Giriş yapmış bir operatörün rolünün izin vermediği bir şeyi yapması | Operation sarmalayıcısı, operation'ın kendi bildiriminden. Yedek kanca cevap veriyorsa **kodunuz** |
| API kapsamı | Geçerli bir kimlik bilgisinin kendisine verilmemiş bir uca ulaşması | Çekirdek, rota yalnız-kimlik bayrağını kurmadıysa; kurduysa geçerli her anahtar geçer |
| Sahiplik kapsamı | Bir müşterinin başka bir müşterinin satırlarını okuması | Her sorguda **kodunuz**, enjekte edilen sahipten, istek gövdesinden asla |
| Form kapıları | Siteler arası gönderim, sel, botlar ve spam içeriği | **Kodunuz**, sabit sırayla dört ya da beş çağrı, artı iki şablon etiketi |

## Adım Adım

### Girdiyi Güvenle Okuyun

1. Bir süper globale asla dokunmayın. Kaynak önekiyle filtre yardımcısından okuyun.
2. Filtre tipini alışkanlıktan değil, alanın *ne olduğundan* seçin.
3. Filtrelemek doğrulamak değildir. Tip filtresi ait olamayacakları çıkardıktan sonra kalanın kullanılabilir olduğunu kontrol edin.
4. Her dizi değerini varsayılanla okuyun ve aynı ifade içinde tip dönüşümü yapın. Döngüdeki eksik anahtar her turda bir disk yazımına yol açar.
5. Varlık kontrolünü `false` ile yazın, `null` ile asla.

### Çağıranı Yetkilendirin

1. Normal bir admin operation'ında yetki listesini operation'ın kendi özelliklerinde bildirin. Sarmalayıcı, metodunuz çalışmadan önce onu kontrol eder.
2. Yedek kancanın cevapladığı operation'da yetkiyi kendiniz, dinleyicide ilk iş olarak kontrol edin. Sarmalayıcı zaten reddetmiş, ona ait hiçbir şey geçerli değil. Ne yetki listesi, ne demo kipi kapısı, ne de AJAX-dışı isteği reddeden kontrol.
3. Bir modül API rotasında kapsam ile yalnız-kimlik bayrağı arasında bilinçli seçin. Yalnız-kimlik, doğru hedef kitleden geçerli her kimlik bilgisini içeri alır. Serbest ekran için makul, yazan hiçbir şey için değil.
4. Müşteriye dönük her uçta kimliği enjekte edilen sahipten alın. İstek gövdesindeki bir hesap kimliği çağıranın önerisi, cevabı değil.

### Açık Bir Formu Koruyun

1. Jeton etiketini form öğesinin içine, captcha etiketini de formun captcha isteyebildiği yere koyun.
2. Jetonu diğer kapılardan önce, demo kapısının ardından doğrulayın. Doğrulama varsayılan olarak AJAX başlığını bekler. Normal gönderim yapan form üçüncü argümanla AJAX-dışı bayrağını geçer.
3. Önce sert bloğu, sonra captcha zorunluluğunu kontrol edin. Spam kapısı en sonda, alanlar okunduktan sonra gelir.
4. İsteği sonda sayın ve captcha çözülüp çözülmediğine göre kalkan penceresini temizleyin ya da kaydedin.
5. Formunuzu spam kapısının çağrı listesine ekleyin. Bunu atlayan bir misafir sayfasında dört bot katmanı var ama içerik kuralları yok.

### Yüklenen Dosyayı Sunun

1. Yardımcı üzerinden akıtın. İçerik tipi başlığını tespit edilen tipten kendiniz asla yazmayın.
2. Yükleme dizinine doğrudan erişimi bir kural dosyasıyla kapatın.
3. Satır içinde gösterilmesi gereken görsel dizinlerine dokunmayın.
4. Üçünü de doğrulayın. Statik yol yasak döner, görsel yolu düzgün cevaplar. Controller ucu işaretlemeyi indirmeye zorlar, düz metni satır içinde gösterir.

## Referans

### Girdiyi Okumak

```php
// $arg, "SOURCE/key" ya da "SOURCE/key/subkey" olur; kaynaklar: GET/ POST/ REQUEST/ FILES/ SERVER/
// $mod filtre tipidir; $special bazı tiplerin izinli karakter kümesine karakter ekler.
public static function init($arg = NULL, $mod = false, $special = false);

// Tek bir kaynağa ham erişim. İç içe anahtarlar eğik çizgiyle. Olmayan anahtar => false.
public static function GET($arg = '');
public static function POST($arg = '');
public static function REQUEST($arg = '');
public static function SERVER($arg = '');

// İzinli etiket sıyırma; "hclear" tipinin kullandığı ve doğrudan da çağrılabilen metot.
public static function html_clear($arg = NULL, $allow = '');
```

| Tip | Ondan geriye ne kalır | Ne için kullanılır |
| --- | --- | --- |
| `password` | **Her şey.** Argümanı dokunmadan döndüren bilinçli bir geçirgenlik | Her türden sır. Başka her tip, onları güçlü kılan karakterleri siler |
| `hclear` | Etiketleri sıyrılmış metin | Başka bir biçimi olmayan serbest metin: bir not, bir konu satırı, bir ad |
| `rnumbers` | Bir tam sayı | Kayıt kimlikleri, sayılar, birazdan bir sorguya koyacağınız her şey |
| `numbers` | Rakamlar ve tire, dize olarak | Telefon numaraları ve referans kodları, baştaki sıfırın önemli olduğu yerler |
| `amount`, `rate` | Ayraçlı rakamlar ve sırasıyla bir ondalık sayı | Para ve yüzdeler. Kaçak bir harfi elinde tutan düz metin filtresi asla |
| `email` | Yalnız adres karakter kümesi | Adresler, üstüne bir de biçim kontrolü: filtre çıkarır, doğrulamaz |
| `ip` | Adres karakterleri | Adresler ve aralıklar. Bir şey karşılaştırmadan önce gerçek bir ayrıştırma yapın |
| `route` | Harf, rakam, tire, alt çizgi ve nokta; üst dizin geçişi önce silinir | Bir yolun ya da rota anahtarının parçası olacak her şey |
| `letters_numbers` | Harf ve rakamlar, artı üçüncü argümanın izin verdikleri | Biçimini sizin belirlediğiniz tanımlayıcılar: veritabanı adı, modül anahtarı, slug |
| `domain` | Harf-rakam, nokta ve tire; küçük harfe çevrilir. Doğrulama **yapmaz**: `not a domain!!!` girdisi `notadomain` olarak geri döner | Alan adları, üstüne gerçek bir kontrol koyarak; tıpkı adres filtresi gibi. Üçüncü argümanı da yok sayar |

> **Olmayan anahtar false döner, yani null'a karşı yazılmış kapı hep açıktır**
> 
> Hem tipli okuyucu hem doğrudan kaynak okuyucuları, olmayan anahtarda `false` döner. `null` asla dönmezler. Null'a eşit değil koşulu her istekte doğru, arkasındaki dal koşulsuz çalışır. Bu yayına çıktı. Böyle korunan bir kip işareti her ziyarette bir simülasyon katmanı yükledi. Hiçbir şey sunucuya ulaşmadı, yanıtlar yine de başarılı göründü. Boş dizeyle karşılaştırmadan önce dizeye çevirin.

### Yetkilendirme

```php
// coremio/helpers/admin.php - oturumdaki operatör yetkiyi taşıyorsa true.
public static function isPrivilege($privileges): bool;

// coremio/api/Auth/Scope.php - kimlik bilgisinin izinleri, rotanın gereksinimine karşı.
public function __construct(array $granted);
public function allows(string $required): bool;

// coremio/api/Resources/Client/_ClientResource.php - isteğin adına hareket ettiği müşteri.
protected function owner(): int;
protected function assertOwned(mixed $row, string $message = 'Resource not found.'): array;
```

- **Admin::isPrivilege()**: Normal bir operation'ın bildirdiği diziyi alır. Orada sarmalayıcı onu sizin için çağırır ve metodunuz koşmadan reddeder. Yedek kanca dinleyicisinde kimse çağırmaz, siz çağırırsınız.
- **Scope::allows()**: İzinler `Grup/Eylem` dizeleridir ve üç şekilde eşleşir: birebir dize, sondaki joker ile tüm grup ya da her şey anlamına gelen tek joker. Boş bir gereksinim geçer: kurulmamış bir rota kapsamı açık bir rotadır.
- **owner()**: Adına hareket edilen müşteri; çekirdek tarafından dış çağrıda kimlik bilgisinden, iç çağrıda girdiden enjekte edilir. Sıfır döndürmek yerine hata fırlatır; hiçbir sorgu sessizce kapsamsız çalışmaz.
- **assertOwned()**: Hem "yok" hem "başkasına ait" durumunu aynı bulunamadı cevabına çevirir. Ayrım bilginin kendisidir: sızdırmak, çağıranın hangi kimliklerin gerçek olduğunu saymasına izin verir.

### Açık Bir Formu Kapıya Almak

Bağımsız beş katman, bu sırayla. İlk dördü kimin sorduğunu, beşincisi ne gönderdiğini yargılar.

| Sıra | Çağrı | Hayır dediğinde |
| --- | --- | --- |
| 1 | `Validation::verify_csrf_token($token, $key, $nonAjax = false)` | Başka hiçbir iş yapılmadan hemen reddedin. Anahtar, şablonun bastığıyla aynı olmalı |
| 2 | `ProcessRestriction::blocked($action)` | Bu adres sert blok penceresinin içinde. Hız sınırı mesajıyla cevaplayın ve durun |
| 3 | `Captcha::enabled($area)` ya da `BotShield::triggered($action)` | Captcha gerekiyor. Denemeyi kaydedin ve captcha gerekli durumuyla cevaplayın |
| 4 | `Validation::spam_guard($subject, $message, $email, $phone, $ip, $domain)` | Boş olmayan sonuç engellendi demektir. Dönen gerekçe loga gider, ziyaretçiye asla |
| 5 | `ProcessRestriction::hit($action)` artı kalkanı kaydet ya da temizle | Asıl işten sonra. Bunu atlamak, kurulmuş görünen bir sınırın neden hiç ateşlenmediğidir |

```php
// coremio/classes/Validation.php
public static function get_csrf_token($form_index = '', $input = true);
public static function verify_csrf_token($incoming_data = '', $form_index = '', $nonAjax = false);
public static function spam_guard(string $subject = '', string $message = '', string $email = '',
                                  string $phone = '', string $ip = '', string $domain = ''): string;
public static function password_chars_error($password = ''): string;

// coremio/helpers/processrestriction.php - null ip "çağıranınki" demek, proxy'yi bilir.
public static function blocked(string $action, ?string $ip = NULL): bool;
public static function hit(string $action, ?string $ip = NULL): bool;
public static function clear(string $action, ?string $ip = NULL): void;

// coremio/helpers/botshield.php - uyarlanabilir captcha, adres başına sayılır.
public static function active(string $action): bool;
public static function triggered(string $action, ?string $ip = NULL): bool;
public static function record(string $action, ?string $ip = NULL): void;
public static function clear(string $action, ?string $ip = NULL): void;

// coremio/helpers/captcha.php - sabit ayar ve cevap kontrolü.
public static function enabled(string $area = ''): bool;
public function check(): bool;

// coremio/classes/FraudModule.php - yalnız sipariş anında. Boş dize temiz demektir.
public static function run_checks(array $params = []): string;
```

- **spam_guard() gerekçe döner, boolean değil**: Boş dize temiz demektir; başka her şey, engellenenler listesine zaten yazılmış, operatöre dönük gerekçedir. Ziyaretçiye bunun yerine genel çevrilmiş mesajı gösterin. Gerekçe hangi kuralın ateşlendiğini söyler, yani saldırgan için bir ayar kılavuzudur.
- **Jeton anahtarı birebir bir sözleşmedir**: Şablonun bastığı dize ile işleyicinin doğruladığı dize aynı olmak zorundadır. Rotadan türetilmezler, yani uyumsuzluk göreceğiniz bir hata değildir. Her gönderim doğrulamadan geçemez.
- **Eşikler yapılandırmada yaşar**: Deneme sayıları, pencereler ve blok süreleri, Güvenlik ekranlarının yazdığı seçenek dosyasında operatörün sahibi olduğu ayarlardır. Modülünüze yazılmış bir sayı, operatör kendi ayarını değiştirdiği anda çalışmayı bırakır.
- **Sahtekârlık kontrolleri ayrı bir katmandır**: Sipariş oluşturulurken, hiçbir şey kalıcı hâle gelmeden koşarlar ve spam kapısı değildirler. Hata fırlatan bir modül geçti sayılır ve loglanır, yani bir sağlayıcı kesintisi ödeme akışını götüremez.

### Çıktı ve Dosyalar

```php
// coremio/classes/Utility.php - her yanıtın içinden geçtiği JSON kodlayıcı.
public static function jencode($string = '', $flags = 0): string|false;
public static function jdecode($string = '', $mode = false);

// coremio/classes/Utility.php - saklı bir dosyayı tarayıcıya vermenin desteklenen tek yolu.
public static function stream_uploaded_file(string $diskPath, string $fileName, array $opt = []): void;
```

- **stream_uploaded_file()**: Satır içinde yalnız sabit ve etkisiz bir küme için sunar: taşınabilir belgeler, dört raster görsel tipi ve düz metin. İşaretleme ve vektörel görseller dahil geri kalan her şey indirmeye zorlanır; genel bir tip, koklama-yok başlığı ve kum havuzu politikası taşır. Dosya adından satır sonlarını ve tırnakları siler, içeriği gönderir ve çıkar.
- **Önbellek seçeneği yalnız içerikten türeyen adresler içindir**: İsteğe bağlı üçüncü argüman önbellek politikasını ezer; dosya değiştiğinde değişen adresler içindir. Hassas bir belge onu geçmemelidir: kısa ve özel varsayılan meselenin kendisidir.
- **Utility::jencode()**: Ham kodlayıcı yerine kullanılır, böylece her yanıt tek bir seçenek kümesini paylaşır. Latin dışı karakterler ve eğik çizgiler kaçırılmak yerine okunur kalır. Kodlamak kaçırmak değildir: işaretlemenin içine basılacak bir değer, basıldığı noktada yine kaçırılmalıdır.
- **htmlspecialchars()**: Admin şablonları otomatik kaçırma yapmayan düz PHP'dir, yani oraya bastığınız her şey basıldığı noktada kaçırılır. Website şablonları varsayılan olarak kaçırır, yani ters tuzak. Oraya önceden kodlanmış bir değer saklamak onu iki kez kodlar ve ekranda varlıkları gösterir.

## Örnek

Baştan sona bir misafir formu. Şablon iki etiketi gösterir, işleyici beş katmanı sırayla uygular.

```smarty
<form action="{link route='acme-request'}" method="post" data-results="#acme-out">
    <input type="text"  name="company">
    <input type="email" name="email">
    <input type="password" name="panel_password">
    <textarea name="note"></textarea>

    {* buradaki anahtar ile işleyicideki anahtar tek bir literal sözleşmedir *}
    {csrf form='acme-request'}
    {captcha area='acme-request' tray='acme-captcha'}
</form>
```

```php
public function submit(\Operation $operation): bool
{
    // Sarmalayıcı her operation'a bir Operation nesnesi verir. Tanıtım kapısıyla açın:
    // bir tanıtım sisteminde herhangi bir yazma gerçekleşmeden önce fırlatır.
    $operation->demo();

    // 1. Jeton, diğer her kapıdan önce. AJAX olmayan gönderimde üçüncü argüman true.
    if (!\Validation::verify_csrf_token((string) Filter::init('POST/token', 'hclear'), 'acme-request'))
        return $operation->output(['status' => 'error', 'message' => Language::g('needs/csrf-failed')]);

    // 2. Bu adres için sert engel penceresi.
    if (\ProcessRestriction::blocked('acme-request'))
        return $operation->output(['status' => 'error', 'message' => Language::gc('acme/too-many')]);

    // 3. Captcha: operatörün sabit ayarı YA DA uyarlanabilir kalkanın devreye girmesi.
    $needCaptcha = \Captcha::enabled('acme-request') || \BotShield::triggered('acme-request');

    if ($needCaptcha && !(new \Captcha())->check()) {
        \BotShield::record('acme-request');
        return $operation->output(['status' => 'captcha_required']);
    }

    // Alan başına bir çağrı ve tip, alanın NE OLDUĞUNA göre seçilir.
    // Sır, geçirgen filtreyi kullanır: herhangi bir metin filtresi, tam da parolayı
    // güçlü kılan noktalama işaretlerini sessizce silerdi.
    $company  = Filter::init('POST/company', 'hclear');
    $email    = Filter::init('POST/email', 'email');
    $note     = Filter::init('POST/note', 'dtext');
    $password = Filter::init('POST/panel_password', 'password');

    // Filtreleme oraya ait olamayacakları çıkardı; kalanın kullanılabilir olup olmadığına doğrulama karar verir.
    if (!filter_var($email, FILTER_VALIDATE_EMAIL))
        throw new Exception(Language::gc('acme/email-invalid'));

    if ($e = \Validation::password_chars_error($password)) throw new Exception($e);

    // 4. İçerik ve gönderen kuralları; alanlar okunduktan sonra, asıl işten önce.
    if (\Validation::spam_guard('', $note, $email, '', UserManager::GetIP()) !== '')
        throw new Exception(Language::g('needs/spam-blocked'));

    // ... asıl iş

    // 5. Bu isteği sayın ve kalkan penceresini sonuçlandırın.
    \ProcessRestriction::hit('acme-request');
    if ($needCaptcha) \BotShield::clear('acme-request');
    else              \BotShield::record('acme-request');

    return $operation->output(['status' => 'successful']);
}
```

Diğer iki ekran, kapıyı kaçınılmaz kılan biçimde.

```php
public function GetNote(array $body = []): array
{
    $id = (int) ($body['id'] ?? 0);

    // Sahip, kimlik bilgisinden gelir. Gövdedeki bir hesap kimliği çağıranın ÖNERİSİDİR
    // ve hesabı seçmek için asla kullanılmaz.
    $row = WDB::select('*')->from('acme_notes')
              ->where('id', '=', $id)
              ->where('owner_id', '=', $this->owner())
              ->build('assoc');

    // Olmayan da yabancı da 404 döner: hangi kimliklerin var olduğu başlı başına bilgidir.
    return ['data' => $this->assertOwned($row)];
}

public function stream_note_file(int $id): void
{
    $row = $this->model->note($id);
    if ((int) ($row['owner_id'] ?? 0) !== $this->owner()) { http_response_code(404); exit; }

    // Bu başlıkları asla tespit edilen tipten elle kurmayın: satır içinde sunulan,
    // yüklenmiş bir sayfa bu köken üzerinde, onu açan sonraki operatöre karşı çalışır.
    \Utility::stream_uploaded_file(ROOT_DIR . $row['path'], $row['name']);
}
```

```apache
# Yukarıdaki sahiplik kontrolü PHP'de yaşar; web sunucusunun ondan haberi yoktur ve
# aynı baytları doğrudan yoldan sunar. Dosyayı diskten okumak bir dosya sistemi
# işlemidir ve etkilenmez, yalnız doğrudan HTTP yolu kapanır.
# Her iki söz dizimi de yazılır; ağaçtaki her özel yükleme dizini zaten böyle yazılmıştır:
# 2.4 yönergesi tek başına, mod_authz_core olmayan bir sunucuda hata verir.
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
    Order allow,deny
    Deny from all
</IfModule>
```

## Tuzaklar

> **Genel bir temizleyici ilk olarak sır alanlarını bozar**
> 
> Metin filtreleri noktalama işaretlerini çıkarır, parolayı güçlü kılan şey de noktalama. Birini bir parola alanında çalıştırın, değer sessizce kısalır. Hesap, müşterinin hiç yazmadığı bir değerle açılır, arıza sonradan giriş hatası olarak çıkar. Alan başına filtreleyin, sırlara geçirgen tipi verin.

> **Web'den erişilebilen yükleme dizininde erişim kontrolü yok**
> 
> Controller'ınızdaki her sahiplik kontrolü, dosya yolu doğrudan istenerek atlanır. Sunucu yüklenmiş bir sayfayı sayfa olarak sunar. İki yarı da gerekli. Akış yardımcısı tipi etkisiz hâle zorlar, ret kuralı statik yolu kapatır.

> **Boşluk testleri saklanmış bir sıfırı yok sayar**
> 
> Boşluk testi sıfır dizesi için doğru sonuç verir. Operatörün açıkça kapattığı bir ayar, hiç yapılandırılmamış gibi okunur ve kod varsayılanına düşer. Anahtarı varsayılanla okuyup aynı ifadede tip dönüşümü yapın, sonra karşılaştırın.

> **Çağıranın verdiği bir adrese asla istek atmayın**
> 
> Çağıranın seçtiği bir adrese giden dış istek, içerik ağının arkasındaki origin'i adres sahibine ifşa eder. Müşteriye dönük uçlar bunun yerine yüklenen baytları alır. Bir adresi ancak operatör ekranında ya da sağlayıcının imzaladığı bir kaynaktan getirin.

## İlgili Makaleler

- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
- [Tema Formlarını Güvenli Hale Getirme](https://dev.wisecp.com/tr/tema-formlarini-guvenli-hale-getirme)
- [API Kimlik Doğrulama ve İzinler](https://dev.wisecp.com/tr/api-kimlik-dogrulama-ve-izinler)
- [Müşteri API'si](https://dev.wisecp.com/tr/musteri-api-giris)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [Güncellemeye Dayanıklı Çalışma İlkeleri](https://dev.wisecp.com/tr/guncellemeye-dayanikli-calisma-ilkeleri)
