Перейти к содержимому

Предотвращение SQL инъекций

SQL инъекция - одна из самых опасных и распространённых уязвимостей веб-приложений. Это руководство описывает, как защитить ваши модули XOOPS от атак SQL инъекций.

  • Security-Best-Practices - Комплексное руководство по безопасности
  • CSRF-Protection - Система токенов и класс XoopsSecurity
  • Input-Sanitization - MyTextSanitizer и валидация

SQL инъекция происходит, когда пользовательский ввод включается непосредственно в SQL запросы без надлежащей санитизации или параметризации.

// ОПАСНО - НЕ ИСПОЛЬЗУЙТЕ
$id = $_GET['id'];
$sql = "SELECT * FROM " . $xoopsDB->prefix('items') . " WHERE id = " . $id;
$result = $xoopsDB->query($sql);

Если пользователь передаёт 1 OR 1=1 как ID, запрос становится:

SELECT * FROM xoops_items WHERE id = 1 OR 1=1

Это возвращает все записи вместо одной.

Использование параметризованных запросов

Заголовок раздела «Использование параметризованных запросов»

Наиболее эффективная защита от SQL инъекций - использование параметризованных запросов (подготовленных выражений).

// Получение подключения к БД
$xoopsDB = XoopsDatabaseFactory::getDatabaseConnection();
// БЕЗОПАСНО - Использование параметризованного запроса
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$_GET['id']]);
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" WHERE username = ? AND status = ?";
$result = $xoopsDB->query($sql, [$username, $status]);

Некоторые уровни абстракции БД поддерживают именованные параметры:

$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" WHERE username = :username AND status = :status";
$result = $xoopsDB->query($sql, [
':username' => $username,
':status' => $status
]);

XOOPS предоставляет объектно-ориентированный доступ к БД, который помогает предотвратить SQL инъекции через систему Criteria.

// Get the handler
$itemHandler = xoops_getModuleHandler('item', 'mymodule');
// Create criteria
$criteria = new Criteria('category_id', (int)$categoryId);
// Get objects - automatically safe from SQL injection
$items = $itemHandler->getObjects($criteria);
$criteria = new CriteriaCompo();
$criteria->add(new Criteria('category_id', (int)$categoryId));
$criteria->add(new Criteria('status', 'published'));
$criteria->add(new Criteria('uid', (int)$userId));
// Опционально: добавление сортировки и пределов
$criteria->setSort('created');
$criteria->setOrder('DESC');
$criteria->setLimit(10);
$criteria->setStart(0);
$items = $itemHandler->getObjects($criteria);
// Equal (default)
$criteria->add(new Criteria('status', 'active'));
// Not equal
$criteria->add(new Criteria('status', 'deleted', '!='));
// Greater than
$criteria->add(new Criteria('count', 100, '>'));
// Less than or equal
$criteria->add(new Criteria('price', 50, '<='));
// LIKE (for partial matching)
$criteria->add(new Criteria('title', '%' . $searchTerm . '%', 'LIKE'));
// IN (multiple values)
$criteria->add(new Criteria('id', '(' . implode(',', $ids) . ')', 'IN'));
$criteria = new CriteriaCompo();
$criteria->add(new Criteria('status', 'published'));
// OR condition
$orCriteria = new CriteriaCompo();
$orCriteria->add(new Criteria('uid', (int)$userId), 'OR');
$orCriteria->add(new Criteria('is_public', 1), 'OR');
$criteria->add($orCriteria);

Всегда используйте систему префиксов таблиц XOOPS:

// Correct - using prefix
$table = $xoopsDB->prefix('mytable');
$sql = "SELECT * FROM {$table} WHERE id = ?";
// Also correct
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$sql = "INSERT INTO " . $xoopsDB->prefix('mytable') .
" (title, content, uid, created) VALUES (?, ?, ?, ?)";
$result = $xoopsDB->query($sql, [
$title,
$content,
(int)$userId,
time()
]);
if ($result) {
$newId = $xoopsDB->getInsertId();
}
// Create new object
$item = $itemHandler->create();
// Set values - handler escapes automatically
$item->setVar('title', $title);
$item->setVar('content', $content);
$item->setVar('uid', (int)$userId);
$item->setVar('created', time());
// Insert
if ($itemHandler->insert($item)) {
$newId = $item->getVar('itemid');
}
$sql = "UPDATE " . $xoopsDB->prefix('mytable') .
" SET title = ?, content = ?, updated = ? WHERE id = ?";
$result = $xoopsDB->query($sql, [
$title,
$content,
time(),
(int)$id
]);
// Get existing object
$item = $itemHandler->get((int)$id);
if ($item) {
$item->setVar('title', $title);
$item->setVar('content', $content);
$item->setVar('updated', time());
$itemHandler->insert($item);
}
$sql = "DELETE FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$id]);
$item = $itemHandler->get((int)$id);
if ($item) {
$itemHandler->delete($item);
}
$criteria = new Criteria('status', 'deleted');
$itemHandler->deleteAll($criteria);

If you cannot use prepared statements, use proper escaping:

// Using mysqli_real_escape_string
$safe_value = $xoopsDB->escape($value);
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" WHERE title = '" . $safe_value . "'";

However, always prefer prepared statements over escaping.

Column names cannot be parameterized. Validate against a whitelist:

$allowed_columns = ['title', 'created', 'updated', 'status'];
$sort = $_GET['sort'] ?? 'created';
if (!in_array($sort, $allowed_columns)) {
$sort = 'created'; // Default safe value
}
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" ORDER BY {$sort} DESC";

Similarly, validate table names:

$allowed_tables = ['items', 'categories', 'comments'];
$table = $_GET['table'] ?? 'items';
if (!in_array($table, $allowed_tables)) {
$table = 'items';
}
$sql = "SELECT * FROM " . $xoopsDB->prefix($table) . " WHERE id = ?";
$criteria = new CriteriaCompo();
// Add conditions based on input
if (!empty($_GET['category'])) {
$criteria->add(new Criteria('category_id', (int)$_GET['category']));
}
if (!empty($_GET['status'])) {
$allowed_statuses = ['draft', 'published', 'archived'];
if (in_array($_GET['status'], $allowed_statuses)) {
$criteria->add(new Criteria('status', $_GET['status']));
}
}
if (!empty($_GET['search'])) {
$search = '%' . $_GET['search'] . '%';
$criteria->add(new Criteria('title', $search, 'LIKE'));
}
$items = $itemHandler->getObjects($criteria);

Be careful with LIKE queries to avoid wildcard injection:

// Escape special characters in search term
$searchTerm = str_replace(['%', '_'], ['\%', '\_'], $searchTerm);
// Then use in LIKE
$criteria->add(new Criteria('title', '%' . $searchTerm . '%', 'LIKE'));

When using IN clauses, ensure all values are properly typed:

// Array of IDs from user input
$inputIds = $_POST['ids'] ?? [];
// Sanitize: ensure all are integers
$safeIds = array_map('intval', $inputIds);
$safeIds = array_filter($safeIds, function($id) { return $id > 0; });
if (!empty($safeIds)) {
$idList = implode(',', $safeIds);
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" WHERE id IN ({$idList})";
$result = $xoopsDB->query($sql);
}

Or with Criteria:

if (!empty($safeIds)) {
$criteria = new Criteria('id', '(' . implode(',', $safeIds) . ')', 'IN');
$items = $itemHandler->getObjects($criteria);
}

When performing multiple related queries:

// Start transaction
$xoopsDB->query("START TRANSACTION");
try {
// Query 1
$sql1 = "INSERT INTO " . $xoopsDB->prefix('items') . " (title) VALUES (?)";
$result1 = $xoopsDB->query($sql1, [$title]);
if (!$result1) {
throw new Exception('Insert failed');
}
$itemId = $xoopsDB->getInsertId();
// Query 2
$sql2 = "INSERT INTO " . $xoopsDB->prefix('item_meta') .
" (item_id, meta_key, meta_value) VALUES (?, ?, ?)";
$result2 = $xoopsDB->query($sql2, [$itemId, 'author', $author]);
if (!$result2) {
throw new Exception('Meta insert failed');
}
// Commit
$xoopsDB->query("COMMIT");
} catch (Exception $e) {
// Rollback on error
$xoopsDB->query("ROLLBACK");
throw $e;
}

Never expose SQL errors to users:

$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$id]);
if (!$result) {
// Log the actual error for debugging
error_log('Database error: ' . $xoopsDB->error());
// Show generic message to user
redirect_header('index.php', 3, 'An error occurred. Please try again.');
exit();
}
// WRONG
$sql = "SELECT * FROM {$table} WHERE id = {$id}";
// RIGHT
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$id]);
// WRONG - addslashes is NOT sufficient
$safe = addslashes($_GET['input']);
// RIGHT - use parameterized queries or proper escaping
$sql = "SELECT * FROM table WHERE col = ?";
$result = $xoopsDB->query($sql, [$_GET['input']]);
// WRONG - assuming input is numeric
$id = $_GET['id'];
$sql = "SELECT * FROM table WHERE id = " . $id;
// RIGHT - explicitly cast to integer
$id = (int)$_GET['id'];
$sql = "SELECT * FROM table WHERE id = ?";
$result = $xoopsDB->query($sql, [$id]);
// Data from database is NOT automatically safe
$userData = $itemHandler->get($id);
$username = $userData->getVar('username');
// WRONG - trusting data from database
$sql = "SELECT * FROM log WHERE username = '" . $username . "'";
// RIGHT - always use parameters
$sql = "SELECT * FROM log WHERE username = ?";
$result = $xoopsDB->query($sql, [$username]);

Test your forms with these inputs to check for SQL injection:

  • ' OR '1'='1
  • 1; DROP TABLE users--
  • 1 UNION SELECT * FROM users--
  • admin'--
  • ' OR 1=1#

If any of these cause unexpected behavior or errors, you have a vulnerability.

Use automated SQL injection testing tools during development:

  • SQLMap
  • Burp Suite
  • OWASP ZAP
  1. Always use parameterized queries (prepared statements)
  2. Use XoopsObject/XoopsObjectHandler when possible
  3. Use Criteria classes for building queries
  4. Whitelist allowed values for columns and table names
  5. Cast numeric values explicitly with (int) or (float)
  6. Never expose database errors to users
  7. Use transactions for multiple related queries
  8. Test for SQL injection during development
  9. Escape LIKE wildcards in search queries
  10. Sanitize IN clause values individually

#security #sql-injection #database #xoops #prepared-statements #Criteria