🛡️Μάθημα 8: SQL Injection & Ασφάλεια

Διαδικτυακός Προγραμματισμός - Πανεπιστήμιο Πελοποννήσου

⚠️ ΠΡΟΣΟΧΗ: Το SQL Injection είναι μία από τις πιο επικίνδυνες ευπάθειες ασφαλείας στις web εφαρμογές! Μπορεί να οδηγήσει σε απώλεια ή κλοπή δεδομένων, διαγραφή πινάκων ή ακόμα και πλήρη έλεγχο της βάσης δεδομένων.

🤔Τι είναι το SQL Injection;

Το SQL Injection είναι μία τεχνική επίθεσης όπου ένας κακόβουλος χρήστης εισάγει (inject) κακόβουλο SQL κώδικα μέσα στα πεδία μιας φόρμας, με στόχο να εκτελέσει μη εξουσιοδοτημένες SQL εντολές στη βάση δεδομένων.

Αυτό συμβαίνει όταν ο προγραμματιστής δεν επαληθεύει και δεν απολυμαίνει σωστά τα δεδομένα που έρχονται από τους χρήστες.

🔥1️⃣ Ο Αρχικός Κώδικας (ΕΥΑΛΩΤΟΣ)

Έστω ότι γράφεις κάτι τέτοιο:

<?php // ΕΥΑΛΩΤΟΣ ΚΩΔΙΚΑΣ - ΜΗΝ ΤΟΝ ΧΡΗΣΙΜΟΠΟΙΗΣΕΤΕ! $customer_name = $_POST['customer_name']; $customer_email = $_POST['customer_email']; $product_id = $_POST['product_id']; $quantity = $_POST['quantity']; $sql = "INSERT INTO orders (customer_name, customer_email, product_id, quantity) VALUES ('$customer_name', '$customer_email', '$product_id', '$quantity')"; $conn->query($sql); ?>
⚠️ Το Πρόβλημα: Όλα τα δεδομένα μπαίνουν κατευθείαν μέσα στη SQL εντολή. Αυτό επιτρέπει SQL Injection!

🚨2️⃣ Τι SQL Injection θα γινόταν;

Αν κάποιος κακόβουλος χρήστης βάλει στο πεδίο customer_name το εξής:

Mario'); DROP TABLE orders; --

Τότε η SQL εντολή που θα εκτελεστεί γίνεται:

INSERT INTO orders (customer_name, customer_email, product_id, quantity) VALUES ('Mario'); DROP TABLE orders; --', '[email protected]', '1', '1')

🔴 Εδώ τι συμβαίνει βήμα-βήμα:

  • Mario'); → Κλείνει πρόωρα το πεδίο του ονόματος
  • DROP TABLE orders; → Διατάζει τη βάση να διαγράψει ΟΛΟ τον πίνακα!
  • -- → Σχολιάζει (αγνοεί) ό,τι υπάρχει μετά
🧨 Αποτέλεσμα: Ο πίνακας orders διαγράφεται στη στιγμή! Όλες οι παραγγελίες χάνονται!

Άλλα Παραδείγματα Επιθέσεων SQL Injection:

-- Διαγραφή βάσης δεδομένων Mario'); DROP DATABASE my_database; -- -- Προσθήκη νέου admin χρήστη '; INSERT INTO users (username, password, role) VALUES ('hacker', 'pass123', 'admin'); --

🧱3️⃣ Γιατί συμβαίνει αυτό;

Γιατί ο PHP κώδικας δεν ξεχωρίζει:

Στην ουσία αφήνει τον χρήστη να γράψει ΚΩΔΙΚΑ μέσα στη φόρμα.

💡 Το Κλειδί: Πρέπει να διαχωρίσουμε πλήρως τα δεδομένα από τις SQL εντολές!

🛡️4️⃣ Πώς προστατευόμαστε; → Prepared Statements

Τα Prepared Statements δεν βάζουν τα δεδομένα μέσα στο SQL ως κείμενο, αλλά τα στέλνουν χωριστά, σαν τιμές (parameters).

Δηλαδή:

✔ Ασφαλής μέθοδος: Prepared Statements

Ακολουθεί ο ασφαλής κώδικας:

<?php // ΑΣΦΑΛΗΣ ΚΩΔΙΚΑΣ με Prepared Statements // 🔹 Βήμα 1: Δημιουργούμε ένα "έτοιμο" SQL με ? στη θέση των τιμών $stmt = $conn->prepare(" INSERT INTO orders (customer_name, customer_email, product_id, quantity) VALUES (?, ?, ?, ?) "); // 🔹 Βήμα 2: Δένουμε τις μεταβλητές με τα ? // "ssii" = τύποι δεδομένων: string, string, integer, integer $stmt->bind_param("ssii", $customer_name, $customer_email, $product_id, $quantity); // 🔹 Βήμα 3: Εκτελούμε με ασφάλεια $stmt->execute(); // 🔹 Κλείνουμε τον prepared statement $stmt->close(); ?>

Τι συμβαίνει αν κάποιος προσπαθήσει επίθεση;

Αν κάποιος βάλει:

Mario'); DROP TABLE orders; --

Η βάση το βλέπει σαν απλό κείμενο, π.χ.:

customer_name = "Mario'); DROP TABLE orders; --"

Και το βάζει σαν κείμενο στον πίνακα, ΔΕΝ το εκτελεί!

📊Σύγκριση: Ευάλωτος vs Ασφαλής Κώδικας

❌ ΛΑΘΟΣ (Ευάλωτος)

$sql = "INSERT INTO orders VALUES ('$name', '$email')"; $conn->query($sql);

Κίνδυνος: SQL Injection

✅ ΣΩΣΤΟ (Ασφαλές)

$stmt = $conn->prepare("INSERT INTO orders VALUES (?, ?)"); $stmt->bind_param("ss", $name, $email); $stmt->execute();

Ασφάλεια: Προστατευμένο!

🔑Τύποι Δεδομένων στο bind_param()

Η παράμετρος "ssii" καθορίζει τους τύπους των δεδομένων:
  • s = string (κείμενο)
  • i = integer (ακέραιος αριθμός)
  • d = double (δεκαδικός αριθμός)
  • b = blob

Παραδείγματα:

// 2 strings $stmt->bind_param("ss", $name, $email); // 1 string, 2 integers $stmt->bind_param("sii", $name, $age, $id); // 2 strings, 1 integer, 1 double $stmt->bind_param("ssid", $name, $email, $quantity, $price);

Βήματα για Ασφαλή Κώδικα

Βήμα 1: Prepare (Προετοιμασία)

Δημιουργούμε το SQL template με ? placeholders:

$stmt = $conn->prepare("INSERT INTO orders VALUES (?, ?, ?)");

Βήμα 2: Bind (Σύνδεση)

Συνδέουμε τις μεταβλητές με τους placeholders:

$stmt->bind_param("ssi", $name, $email, $product_id);

Βήμα 3: Execute (Εκτέλεση)

Εκτελούμε το statement με ασφάλεια:

$stmt->execute();

Βήμα 4: Close (Κλείσιμο)

Κλείνουμε το statement:

$stmt->close();

⚠️Παράδειγμα: Μη ασφαλής υλοποίηση

Παρακάτω βλέπουμε έναν πλήρη κώδικα που επεξεργάζεται μια παραγγελία, αλλά ΔΕΝ χρησιμοποιεί Prepared Statements. Αυτός ο κώδικας είναι ευάλωτος σε SQL Injection:

⚠️ ΠΡΟΣΟΧΗ: Ο παρακάτω κώδικας είναι μόνο για εκπαιδευτικούς σκοπούς. ΜΗΝ τον χρησιμοποιήσετε σε πραγματική εφαρμογή!
<?php // --- Στοιχεία σύνδεσης με τη βάση --- $servername = "localhost"; $username = "root"; $password = "123456"; $database = "eshop"; // --- Δημιουργία σύνδεσης --- $conn = new mysqli($servername, $username, $password, $database); // --- Έλεγχος σύνδεσης --- if ($conn->connect_error) { die("Αποτυχία σύνδεσης: " . $conn->connect_error); } // --- Λήψη δεδομένων από τη φόρμα --- $customer_name = $_POST['customer_name']; $customer_email = $_POST['customer_email']; $product_id = $_POST['product_id']; $quantity = $_POST['quantity']; $is_member = isset($_POST['is_member']) ? 'yes' : 'no'; // --- Βήμα 3: SELECT για την τιμή του προϊόντος --- $sql = "SELECT * FROM products WHERE id = $product_id"; $result = $conn->query($sql); if ($result->num_rows > 0) { $row = $result->fetch_assoc(); $product_name = $row["name"]; $product_price = $row["price"]; echo "<h3>Πληροφορίες προϊόντος:</h3>"; echo "Όνομα: " . $product_name . "<br>"; echo "Τιμή: " . $product_price . " €<br>"; } else { echo "Δεν βρέθηκε προϊόν με ID: $product_id"; } // --- Βήμα 4: Υπολογισμοί --- $subtotal = $product_price * $quantity; if ($is_member == 'yes') { $discount = $subtotal * 0.10; } else { $discount = 0; } $total = $subtotal - $discount; // --- Εμφάνιση αποτελεσμάτων --- echo "<h3>Σύνοψη Παραγγελίας:</h3>"; echo "Σύνολο: " . number_format($subtotal, 2) . " €<br>"; echo "Έκπτωση: " . number_format($discount, 2) . " €<br>"; echo "<strong>Τελικό Ποσό: " . number_format($total, 2) . " €</strong><br>"; /* --- Βήμα 6: INSERT στον πίνακα customers και orders --- */ // Πρώτα δημιουργούμε τον πελάτη στη βάση $sql_customer = "INSERT INTO customers (name, email) VALUES ('$customer_name', '$customer_email')"; if ($conn->query($sql_customer) === TRUE) { // Παίρνουμε το id του πελάτη που μόλις δημιουργήθηκε $customer_id = $conn->insert_id; // Τώρα δημιουργούμε την παραγγελία $sql_order = "INSERT INTO orders (customer_id, product_id, quantity, total_price) VALUES ($customer_id, $product_id, $quantity, $total)"; if ($conn->query($sql_order) === TRUE) { echo "<br><strong>Η παραγγελία αποθηκεύτηκε επιτυχώς!</strong>"; } else { echo "<br>Σφάλμα κατά την αποθήκευση της παραγγελίας: " . $conn->error; } } else { echo "<br>Σφάλμα κατά τη δημιουργία πελάτη: " . $conn->error; } // Παίρνουμε το ID της παραγγελίας που μόλις δημιουργήθηκε $order_id = $conn->insert_id; echo "<h2>Η παραγγελία καταχωρήθηκε με επιτυχία!</h2>"; echo "<h3>Στοιχεία Πελάτη</h3>"; echo "Όνομα: " . $customer_name . "<br>"; echo "Email: " . $customer_email . "<br>"; echo "Μέλος: " . ($is_member == 'yes' ? 'Ναι (10% έκπτωση)' : 'Όχι') . "<br><br>"; echo "<h3>Στοιχεία Παραγγελίας</h3>"; echo "Αριθμός Παραγγελίας: " . $order_id . "<br>"; echo "Προϊόν: " . $product_name . "<br>"; echo "Ποσότητα: " . $quantity . "<br>"; echo "Τιμή Μονάδας: " . number_format($product_price, 2) . " €<br>"; echo "Σύνολο: " . number_format($subtotal, 2) . " €<br>"; echo "Έκπτωση: " . number_format($discount, 2) . " €<br>"; echo "<strong>Τελικό Ποσό: " . number_format($total, 2) . " €</strong><br><br>"; echo '<a href="welcome.html">➡️ Νέα Παραγγελία</a><br>'; echo '<a href="view_orders.php">📄 Προβολή Όλων των Παραγγελιών</a>'; $conn->close(); ?>
🔴 Τα προβλήματα ασφαλείας:
  • Τα δεδομένα από τη φόρμα μπαίνουν απευθείας στις SQL εντολές
  • Δεν υπάρχει καμία προστασία από SQL Injection
  • Ένας επιτιθέμενος μπορεί να διαγράψει ή να κλέψει δεδομένα
  • Όλα τα πεδία ($customer_name, $customer_email, $product_id, κλπ.) είναι ευάλωτα

Ασφαλής υλοποίηση με Prepared Statements

Τώρα ας δούμε τον ίδιο ακριβώς κώδικα, αλλά γραμμένο με Prepared Statements για πλήρη προστασία από SQL Injection:

✅ ΑΣΦΑΛΗΣ ΚΩΔΙΚΑΣ: Ο παρακάτω κώδικας χρησιμοποιεί prepared statements σε όλες τις SQL εντολές και είναι ασφαλής για παραγωγική χρήση!
<?php // --- Στοιχεία σύνδεσης με τη βάση --- $servername = "localhost"; $username = "root"; $password = "123456"; $database = "eshop"; // --- Δημιουργία σύνδεσης --- $conn = new mysqli($servername, $username, $password, $database); // --- Έλεγχος σύνδεσης --- if ($conn->connect_error) { die("Αποτυχία σύνδεσης: " . $conn->connect_error); } // --- Λήψη δεδομένων από τη φόρμα --- $customer_name = $_POST['customer_name']; $customer_email = $_POST['customer_email']; $product_id = $_POST['product_id']; $quantity = $_POST['quantity']; $is_member = isset($_POST['is_member']) ? 'yes' : 'no'; // --- Βήμα 3: SELECT για την τιμή του προϊόντος --- // ❌ Παλιά ευάλωτη (χωρίς prepared statement): // $sql = "SELECT * FROM products WHERE id = $product_id"; // $result = $conn->query($sql); // ✅ Νέα ασφαλής έκδοση με prepared statement: $stmt = $conn->prepare("SELECT * FROM products WHERE id = ?"); $stmt->bind_param("i", $product_id); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows > 0) { $row = $result->fetch_assoc(); $product_name = $row["name"]; $product_price = $row["price"]; echo "<h3>Πληροφορίες προϊόντος:</h3>"; echo "Όνομα: " . $product_name . "<br>"; echo "Τιμή: " . $product_price . " €<br>"; } else { echo "Δεν βρέθηκε προϊόν με ID: $product_id"; } // --- Βήμα 4: Υπολογισμοί --- $subtotal = $product_price * $quantity; if ($is_member == 'yes') { $discount = $subtotal * 0.10; } else { $discount = 0; } $total = $subtotal - $discount; // --- Εμφάνιση αποτελεσμάτων --- echo "<h3>Σύνοψη Παραγγελίας:</h3>"; echo "Σύνολο: " . number_format($subtotal, 2) . " €<br>"; echo "Έκπτωση: " . number_format($discount, 2) . " €<br>"; echo "<strong>Τελικό Ποσό: " . number_format($total, 2) . " €</strong><br>"; /* --- Βήμα 6: INSERT στον πίνακα customers και orders --- */ // ❌ Παλιά ευάλωτη έκδοση για customers: // $sql_customer = "INSERT INTO customers (name, email) VALUES ('$customer_name', '$customer_email')"; // if ($conn->query($sql_customer) === TRUE) { // ✅ Νέα ασφαλής έκδοση με prepared statement για customers: $stmt = $conn->prepare("INSERT INTO customers (name, email) VALUES (?, ?)"); $stmt->bind_param("ss", $customer_name, $customer_email); if ($stmt->execute() === TRUE) { // Παίρνουμε το id του πελάτη που μόλις δημιουργήθηκε $customer_id = $conn->insert_id; // ❌ Παλιά ευάλωτη έκδοση για orders: // $sql_order = "INSERT INTO orders (customer_id, product_id, quantity, total_price) // VALUES ($customer_id, $product_id, $quantity, $total)"; // if ($conn->query($sql_order) === TRUE) { // ✅ Νέα ασφαλής έκδοση με prepared statement για orders: $stmt2 = $conn->prepare("INSERT INTO orders (customer_id, product_id, quantity, total_price) VALUES (?, ?, ?, ?)"); $stmt2->bind_param("iiid", $customer_id, $product_id, $quantity, $total); if ($stmt2->execute() === TRUE) { echo "<br><strong>Η παραγγελία αποθηκεύτηκε επιτυχώς!</strong>"; } else { echo "<br>Σφάλμα κατά την αποθήκευση της παραγγελίας: " . $conn->error; } } else { echo "<br>Σφάλμα κατά τη δημιουργία πελάτη: " . $conn->error; } // Παίρνουμε το ID της παραγγελίας που μόλις δημιουργήθηκε $order_id = $conn->insert_id; echo "<h2>Η παραγγελία καταχωρήθηκε με επιτυχία!</h2>"; echo "<h3>Στοιχεία Πελάτη</h3>"; echo "Όνομα: " . $customer_name . "<br>"; echo "Email: " . $customer_email . "<br>"; echo "Μέλος: " . ($is_member == 'yes' ? 'Ναι (10% έκπτωση)' : 'Όχι') . "<br><br>"; echo "<h3>Στοιχεία Παραγγελίας</h3>"; echo "Αριθμός Παραγγελίας: " . $order_id . "<br>"; echo "Προϊόν: " . $product_name . "<br>"; echo "Ποσότητα: " . $quantity . "<br>"; echo "Τιμή Μονάδας: " . number_format($product_price, 2) . " €<br>"; echo "Σύνολο: " . number_format($subtotal, 2) . " €<br>"; echo "Έκπτωση: " . number_format($discount, 2) . " €<br>"; echo "<strong>Τελικό Ποσό: " . number_format($total, 2) . " €</strong><br><br>"; echo '<a href="welcome.html">➡️ Νέα Παραγγελία</a><br>'; echo '<a href="view_orders.php">📄 Προβολή Όλων των Παραγγελιών</a>'; $conn->close(); ?>
🎉 Βελτιώσεις στον ασφαλή κώδικα:
  • Όλες οι SQL εντολές (SELECT και INSERT) χρησιμοποιούν prepared statements
  • ✅ Τα δεδομένα δεν μπαίνουν ποτέ απευθείας στο SQL string
  • ✅ Χρήση bind_param() για ασφαλή δέσιμο τιμών
  • ✅ Προστασία από SQL Injection σε όλα τα σημεία
  • ✅ Ο κώδικας είναι έτοιμος για παραγωγή

🔍 Διαφορές μεταξύ των δύο εκδόσεων:

❌ Μη Ασφαλής
$sql = "SELECT * FROM products WHERE id = $product_id"; $result = $conn->query($sql);

Η μεταβλητή μπαίνει κατευθείαν στο SQL

✅ Ασφαλής
$stmt = $conn->prepare("SELECT * FROM products WHERE id = ?"); $stmt->bind_param("i", $product_id); $stmt->execute(); $result = $stmt->get_result();

Χρήση placeholder (?) και bind_param()

📚Σύνοψη

🎯 Τι πρέπει να θυμάστε:
  • ΜΗΝ βάζετε ποτέ δεδομένα χρηστών κατευθείαν στις SQL εντολές
  • ΠΑΝΤΑ χρησιμοποιείτε Prepared Statements
  • Το SQL Injection μπορεί να καταστρέψει τη βάση σας
  • Τα Prepared Statements διαχωρίζουν δεδομένα από κώδικα
  • Χρησιμοποιείτε το σωστό type specifier: s, i, d, b
🔒 Η Ασφάλεια είναι Προτεραιότητα!

Η χρήση Prepared Statements δεν είναι προαιρετική - είναι υποχρεωτική για κάθε επαγγελματική εφαρμογή. Μία μόνο ευπάθεια SQL Injection μπορεί να καταστρέψει ολόκληρη την εφαρμογή σας!

Γιώργος Μπάρδης

Πανεπιστήμιο Πελοποννήσου - Τμήμα Ψηφιακών Συστημάτων