PHP’de SQL enjeksiyonları nasıl önlenir (PDO)

SQL enjeksiyonu, en yaygın şekilde var olan ve en çok zarar veren web uygulaması güvenlik açıklarından biridir. Neyse ki, hem programlama dilleri hem de RDBMS’lerin kendileri, web uygulaması geliştiricilerine veritabanını güvenli bir şekilde sorgulamanın bir yolunu (parametreli SQL sorguları) sağlamak için gelişti.

SQLi saldırılarından korunmak için parametreli sorgular kullanılır. Parametreli sorguların yazılması ve anlaşılması kolaydır ve bu geliştiriciyi bir ifadedeki gerçek değişkenler için yer tutucuları kullanarak tüm SQL ifadesini önceden tanımlamaya zorlar. Geliştirici SQL ifadesi tanımlandıktan sonra her parametreyi sorguya geçirerek veritabanının SQL komutu ile bir kullanıcı tarafından girilen veriler arasında ayrım yapabilmesini sağlar. SQL komutları bir saldırgan tarafından girilirse, parametreli sorgu girdiyi SQL komutunun aksine bir dize olarak değerlendirir.

Uygulama geliştiricileri, özel karakterlerden kaçarak veya bunları kaldırarak kullanıcı girdilerini sterilize etmekten kaçınmalıdır. Çünkü bir saldırganın bu tür korumaları atlamak için kullanabileceği birkaç kodlama hilesi mevcuttur. Geliştiriciler SQL enjeksiyon güvenlik açıklarından kaçınmak için parametreli sorguları kullanmaya devam etmelidir.

SQL Injection saldırılarını önlemenin tek kesin yolu, giriş doğrulaması ve parametreli sorgulardır. Uygulama kodu, kullanıcıdan gelen herhangi bir veriyi asla doğrudan kullanmamalıdır. Geliştirici, yalnızca oturum açma formları gibi web formu girdilerini değil, tüm girdileri dezenfekte etmelidir. Tek tırnak gibi olası kötü amaçlı kod öğelerini kaldırmaları gerekir. Yayına verilen sitelerinizdeki veritabanı hatalarının görünürlüğünü kapatmak da iyi bir fikirdir. Veritabanı hataları, veritabanınız hakkında bilgi edinmek için SQL Injection ile kullanılabilir.

Örneğin bir Acunetix taraması kullanarak bir SQL Injection güvenlik açığı keşfederseniz, bunu hemen düzeltemeyebilirsiniz. Örneğin, güvenlik açığı açık kaynak kodunda olabilir. Bu gibi durumlarda, girişinizi geçici olarak sterilize etmek için bir web uygulaması güvenlik duvarı kullanabilirsiniz.

PHP dilinde SQL Injection saldırılarının nasıl önleneceğini öğrenmek için bakınız. Bunu diğer birçok farklı programlama dilinde nasıl yapacağınızı öğrenmek için SQL Injection’ı önlemeye yönelik Bobby Tables kılavuzuna bakın.

PHP de SQL enjeksiyonların önlenmesi

PHP ve diğer herhangi bir dilde kodlanan web uygulamasını kullanıcı girdilerinden (GET/POST/Headers/Cookier) korumak için öncelikle onları güvensiz kabul etmek gerekir. Programcı girdiler üzerinde birçok temizlik ve kontrol yapabilir fakat bunlardan korunmanın en iyi yolu SQLi saldırılarına karşı geliştirilmiş olan parametrik SQL yapılarını kullanmaktır.

Aşağıdaki kod, bir kimliği kabul eden ve kullanıcının adını gösteren çok basit bir PHP uygulamasıdır. Uygulama GET kullanır, ancak POST veya başka bir HTTP yöntemini kullanabilir. Bu örnek MySQL veritabanına dayanmaktadır ancak aynı ilkeler diğer veritabanları için de geçerlidir. Örnek veritabanı tablosunun adı users’dır ve aşağıdaki yapı/içeriğe sahiptir.

idusernamepasswordfirst_namelast_name
1okoca$2a$10$SakFH.Eatq3QnknC1j1uo.rjM4KIYn.o8gPb6Y2ÖzgürKoca
2guvendem$2a$10$hA/hwCzhr6F23BsbRZBjdOA5eqTgV01cv30syGüvenDemir
3eminkadioglu$2a$10$OkV5tCMMsy91pkkMXHa94OgcunNtuhxsQEminKadıoğlu
4umtchn$2a$10$2NgAjstT9NcN58zMcF/Rq.pYt5bg3iQ6OmdÜmitCihan
users tablosu

SQL Injection güvenlik açığı içeren uygulamanın PHP kodu aşağıdadır.

<?php/*
* Check if the 'id' GET variable is set
* Example - http://localhost/?id=1
*/
if (isset($_GET['id'])){
$id = $_GET['id'];

/* Setup the connection to the database */
$mysqli = new mysqli('localhost', 'dbuser', 'dbpasswd', 'sql_injection_example');

/* Check connection before executing the SQL query */
if ($mysqli->connect_errno) {
printf("Connect failed: %s\n", $mysqli->connect_error);
exit();
}

/* SQL query vulnerable to SQL injection */
$sql = "SELECT username
FROM users
WHERE id = $id";

/* Select queries return a result */
if ($result = $mysqli->query($sql)) {
while($obj = $result->fetch_object()){
print($obj->username);
}
}
/* If the database returns an error, print it to screen */
elseif($mysqli->error){
print($mysqli->error);
}
}

Aşağıda, yukarıdaki güvenlik açığı bulunan uygulamaya yapılabilecek meşru bir HTTP isteği örneği verilmiştir.

http://localhost/?id=1
> okoca

Aşağıda, yukarıdaki güvenlik açığı bulunan uygulamaya yapılabilecek kötü amaçlı bir HTTP isteği örneği verilmiştir.

http://localhost/?id=-1 UNION SELECT password FROM users where id=1
> $2a$10$SakFH.Eatq3QnknC1j1uo.rjM4KIYn.o8gPb6Y2

Bu uygulamanın birkaç sorunu var. Hepsi aşağıda açıklandığı üzere SQL Injection güvenlik açığının oluşmasına katkıda bulunur.

SorunAçıklamaDüzeltme Tavsiyesi
Girdi DoğrulamasıUygulamaya geçirilen kimlik numarasının her zaman bir sayı olacağını biliyoruz. Ancak kod, bunun sayı olup olmadığnı doğrulamaz.
Kullanıcı girdisini doğrulamak, SQL Injection için doğrudan bir çözüm değildir, ancak kötü niyetli kullanıcı verilerinin veritabanı tarafından yorumlanmasını önlememize yardımcı olur.
Veritabanı sorgusunu işlemeden önce, kullanıcı girişini doğrulayın.
Bu durumda, giriş değerinin bir sayı olup olmadığını kontrol etmemiz gerekir.
Kod, SQL Enjeksiyonuna izin verirKod, kullanıcı girdisini (bu örnekte bir GET parametresinden) kabul eder ve bunu doğrudan SQL deyimine dahil eder.
Bu, bir saldırganın sorguya SQL enjekte etmesine, dolayısıyla uygulamayı veritabanına hatalı biçimlendirilmiş bir sorgu göndermesi için kandırmasına olanak tanır.
Kullanıcı girdisi içeren SQL sorgularıyla uğraşırken, parametreli sorgular olarak da bilinen önceden hazırlanmış ifadeleri kullanın.
Parametreli bir sorgu, SQL sorgusunun kullanıcı girdisi olarak ele alınması gereken kısımlarını belirtir.
Hatalar kullanıcıya gösterilirHatalar görüntülenirse, saldırgana olası bir saldırıyı geliştirmek için bilgi sağlayabilir.
Veritabanı türü ve sürümü gibi bilgiler, bir SQL Injection güvenlik açığından yararlanmayı kolaylaştırır.
SQL hatalarını kullanıcıya gösterme. Kullanıcıya bir hata göstermeniz gerekiyorsa, hassas bilgiler vermeyen genel bir hata mesajı kullanın.
Hatalar günlüğe kaydedilmezHata günlükleri sorunları çözmenize yardımcı olur. Ayrıca, birisinin uygulamanıza saldırmaya çalışıp çalışmadığını öğrenmenizi sağlarlar.
Veritabanı hataları günlüğü tutmazsanız, bilgi toplama fırsatını kaçırırsınız. Bu bilgiler, bir saldırgan bir güvenlik açığından yararlanmadan önce uygulamanızın güvenliğini artırmanıza yardımcı olabilir.
Veritabanı hatalarını kullanıcıya göstermek yerine, bunları bir dosyaya kaydedin. Dosya, web sunucusu aracılığıyla bir saldırgan tarafından erişilebilir olmamalıdır.
Hataları PHP hata günlüğüne veya seçtiğiniz başka bir dosyaya kaydedebilirsiniz.
Örnek koddaki SQLi güvenlik açıkları

Şimdide bu güvenlik açığını ortadan kaldırmak için neler yapabileceğimize bakalım.

SQL sorgularını parametrelendirin. Parametreli sorguların yazılması ve anlaşılması kolaydır. Sizi SQL sorgusunu tanımlamaya ve sorguda kullanıcı tarafından sağlanan değişkenler için yer tutucuları kullanmaya zorlarlar. SQL deyimi tanımlandıktan sonra her parametreyi sorguya iletebilirsiniz. Bu, veritabanının SQL komutu ile bir kullanıcı tarafından sağlanan veriler arasında ayrım yapmasını sağlar. Bir saldırgan SQL komutları girerse, parametreleştirilmiş sorgu bunları güvenilmeyen girdi olarak kabul eder ve veritabanı enjekte edilen SQL komutlarını yürütmez. SQL sorgularını düzgün bir şekilde parametreleştirirseniz, veritabanına iletilen tüm kullanıcı girdileri veri olarak değerlendirilir ve hiçbir zaman bir komutun parçası olarak karıştırılamaz.

Not — Özel karakterleri atlayarak veya kaldırarak kullanıcı girişini temizlemeyin. Saldırgan, bu tür bir korumayı atlamak için kodlamayı kullanabilir.

PHP Veri Nesneleri (PDO)

Birçok PHP geliştiricisi, mysql veya mysqli uzantılarını kullanarak veritabanlarına erişir. mysqli uzantısıyla parametreli sorgular kullanmak mümkündür, ancak PHP 5.1 veritabanlarıyla çalışmak için daha iyi bir yol sunmuştur: PHP Veri Nesneleri (PDO). PDO, parametreli sorguların kullanımını kolaylaştıran yöntemler sağlar. Ayrıca kodun okunmasını daha kolay ve daha taşınabilir hale getirir. Diğer güzel bir özelliği de sadece MySQL ile değil, birkaç farklı veritabanı türü ile de çalışabilir.

Aşağıdaki örnek, konunun başında sunulanla aynı uygulamayı temsil etmektedir. Bu geliştirilmiş kod, SQL Injection güvenlik açığını önlemek için parametreli sorgularla PDO kullanır.

<?php

/**
 * Check if the 'id' GET variable is set
 * Example - http://localhost/?id=1
 */
if (isset($_GET['id'])){
  $id = $_GET['id'];
  /**
   * Validate data before it enters the database. In this case, we need to check that
   * the value of the 'id' GET parameter is numeric
   */
   if ( is_numeric($id) == true){
    try{ // Check connection before executing the SQL query 
      /**
       * Setup the connection to the database This is usually called a database handle (dbh)
       */
      $dbh = new PDO('mysql:host=localhost;dbname=sql_injection_example', 'dbuser', 'dbpasswd');
      
      /**
       * Use PDO::ERRMODE_EXCEPTION, to capture errors and write them to
       * a log file for later inspection instead of printing them to the screen.
       */
      $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      
      /**
       * Before executing, prepare statements by binding parameters.
       * Bind validated user input (in this case, the value of $id) to the
       * SQL statement before sending it to the database server.
       *
       * This fixes the SQL injection vulnerability.
       */
      $q = "SELECT username 
          FROM users
          WHERE id = :id";
      // Prepare the SQL query string.
      $sth = $dbh->prepare($q);
      // Bind parameters to statement variables.
      $sth->bindParam(':id', $id);
      // Execute statement.
      $sth->execute();
      // Set fetch mode to FETCH_ASSOC to return an array indexed by column name.
      $sth->setFetchMode(PDO::FETCH_ASSOC);
      // Fetch result.
      $result = $sth->fetchColumn();
      /**
       * HTML encode our result using htmlentities() to prevent stored XSS and print the
       * result to the page
       */
      print( htmlentities($result) );
      
      //Close the connection to the database.
      $dbh = null;
    }
    catch(PDOException $e){
      /**
       * You can log PDO exceptions to PHP's system logger, using the
       * log engine of the operating system
       *
       * For more logging options visit http://php.net/manual/en/function.error-log.php
       */
      error_log('PDOException - ' . $e->getMessage(), 0);
      /**
       * Stop executing, return an Internal Server Error HTTP status code (500),
       * and display an error
       */
      http_response_code(500);
      die('Error establishing connection with database');
    }
   } else{
    /**
     * If the value of the 'id' GET parameter is not numeric, stop executing, return
     * a 'Bad request' HTTP status code (400), and display an error
     */
    http_response_code(400);
    die('Error processing bad or malformed request');
   }
}

Sonuç

Parametreli sorgular, SQL Injection güvenlik açıklarını çözer. Bu örnek, güvenlik açığını gidermek için PDO’yu kullanır, ancak SQL Injection’ı önlemek için mysqli işlevlerini kullanmaya devam edebilirsiniz. Ancak, PDO’nun kullanımı daha kolaydır, daha taşınabilirdir ve adlandırılmış parametrelerin kullanımını destekler (bu örnekte, adlandırılmış parametre olarak :id kullandık).

SQL Enjeksiyonlarını önleme hakkında daha fazla bilgi için OWASP SQL Önleme Hile Sayfasına bakın.

SQL Enjeksiyonuna karşı stratejik ilkeler

SQL Injection güvenlik açıklarını önlemek kolay değildir. Özel önleme teknikleri, SQLi güvenlik açığının alt türüne, SQL veritabanı altyapısına ve programlama diline bağlıdır. Ancak, web uygulamanızı güvende tutmak için izlemeniz gereken bazı genel stratejik ilkeler vardır.

  1. Adım : Farkındalığı artırmak için eğitin ve bunu sürdürün
    Web uygulamanızı güvende tutmak için, web uygulamasını oluşturmaya dahil olan herkesin SQL Enjeksiyonlarıyla ilişkili risklerin farkında olması gerekir. Tüm geliştiricilerinize, yönetim ve veri giriş personelinize ve sistem yöneticilerinize uygun güvenlik eğitimi sağlamalısınız. Onları bu sayfaya yönlendirerek başlayabilirsiniz.
  2. Adım: Herhangi bir kullanıcı girdisine güvenmeyin
    Tüm kullanıcı girişlerini güvenilmeyen olarak kabul edin. Bir SQL sorgusunda kullanılan herhangi bir kullanıcı verisi, SQL Enjeksiyonu riskini beraberinde getirir. Kimliği doğrulanmış ve/veya dahili kullanıcılardan gelen girdileri, genel girdileri ele aldığınız şekilde ele alın.
  3. Adım: Kara listeleri değil, beyaz listeleri kullanın
    Kullanıcı girişini kara listelere göre filtrelemeyin. Akıllı bir saldırgan neredeyse her zaman kara listenizi atlatmanın bir yolunu bulacaktır. Mümkünse, yalnızca katı beyaz listeleri kullanarak kullanıcı girişini doğrulayın ve filtreleyin.
  4. Adım: En son teknolojileri benimseyin
    Daha eski web geliştirme teknolojilerinde SQLi koruması yoktur. Geliştirme ortamının ve dilinin en son sürümünü ve bu ortam/dil ile ilişkili en son teknolojileri kullanın. Örneğin, PHP’de MySQLi yerine PDO kullanın.
  5. Adım: Doğrulanmış mekanizmalar kullanın
    SQLi korumasını sıfırdan oluşturmaya çalışmayın. Çoğu modern geliştirme teknolojisi size SQLi’ye karşı koruma sağlayacak mekanizmalar sunabilir. Tekerleği yeniden icat etmeye çalışmak yerine bu tür mekanizmaları kullanın. Örneğin, parametreli sorguları veya saklı yordamları (stored procedures) kullanın.
  6. Adım: Düzenli olarak tarayın
    SQL enjeksiyonu zafiyetleri, geliştiricileriniz tarafından veya harici kütüphaneler/modüller/yazılımlar aracılığıyla keşfedilebilir. Invicti, Nessus, Acunetix ve SQLMap gibi bir web güvenlik açığı tarayıcısı kullanarak web uygulamalarınızı düzenli olarak taramalısınız.

Referanslar:

https://www.acunetix.com/websitesecurity/php-security-1/
https://www.acunetix.com/websitesecurity/php-security-2/
https://www.acunetix.com/websitesecurity/sql-injection2/
https://www.acunetix.com/blog/web-security-zone/php-security-guide/

Yazar: Özgür Koca

Yazar - Tankado.com

“PHP’de SQL enjeksiyonları nasıl önlenir (PDO)” için 2 yorum

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.

This site uses Akismet to reduce spam. Learn how your comment data is processed.