⚠️ ΠΡΟΣΟΧΗ: Το 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;
🔴 Εδώ τι συμβαίνει βήμα-βήμα:
Mario'); → Κλείνει πρόωρα το πεδίο του ονόματος
DROP TABLE orders; → Διατάζει τη βάση να διαγράψει ΟΛΟ τον πίνακα!
-- → Σχολιάζει (αγνοεί) ό,τι υπάρχει μετά
🧨 Αποτέλεσμα: Ο πίνακας orders διαγράφεται στη στιγμή! Όλες οι παραγγελίες χάνονται!
Άλλα Παραδείγματα Επιθέσεων SQL Injection:
Mario'); DROP DATABASE my_database; --
'; INSERT INTO users (username, password, role) VALUES ('hacker', 'pass123', 'admin'); --
🧱3️⃣ Γιατί συμβαίνει αυτό;
Γιατί ο PHP κώδικας δεν ξεχωρίζει:
- 📌 Τα δεδομένα του χρήστη
- 📌 Από τον SQL κώδικα
Στην ουσία αφήνει τον χρήστη να γράψει ΚΩΔΙΚΑ μέσα στη φόρμα.
💡 Το Κλειδί: Πρέπει να διαχωρίσουμε πλήρως τα δεδομένα από τις SQL εντολές!
🛡️4️⃣ Πώς προστατευόμαστε; → Prepared Statements
Τα Prepared Statements δεν βάζουν τα δεδομένα μέσα στο SQL ως κείμενο, αλλά τα στέλνουν χωριστά, σαν τιμές (parameters).
Δηλαδή:
- Το SQL φτιάχνεται προκαθορισμένα.
- Τα δεδομένα συμπληρώνονται αργότερα.
- Το
', ;, -- και ό,τι άλλο, δεν εκτελείται ως κώδικας, αλλά ως απλό κείμενο.
✔ Ασφαλής μέθοδος: Prepared Statements
Ακολουθεί ο ασφαλής κώδικας:
<?php
$stmt = $conn->prepare("
INSERT INTO orders (customer_name, customer_email, product_id, quantity)
VALUES (?, ?, ?, ?)
");
$stmt->bind_param("ssii", $customer_name, $customer_email, $product_id, $quantity);
$stmt->execute();
$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
Παραδείγματα:
$stmt->bind_param("ss", $name, $email);
$stmt->bind_param("sii", $name, $age, $id);
$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';
$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";
}
$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>";
$sql_customer = "INSERT INTO customers (name, email) VALUES ('$customer_name', '$customer_email')";
if ($conn->query($sql_customer) === TRUE) {
$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;
}
$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';
$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";
}
$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>";
$stmt = $conn->prepare("INSERT INTO customers (name, email) VALUES (?, ?)");
$stmt->bind_param("ss", $customer_name, $customer_email);
if ($stmt->execute() === TRUE) {
$customer_id = $conn->insert_id;
$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;
}
$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 μπορεί να καταστρέψει ολόκληρη την εφαρμογή σας!
Γιώργος Μπάρδης
Πανεπιστήμιο Πελοποννήσου - Τμήμα Ψηφιακών Συστημάτων