コンテンツにスキップ

SQLインジェクション防止

SQLインジェクションは最も危険で一般的なWebアプリケーション脆弱性の1つです。このガイドでは、XOOPSモジュールをSQLインジェクション攻撃から保護する方法をカバーしています。

  • セキュリティベストプラクティス - 包括的なセキュリティガイド
  • CSRF保護 - トークンシステムとXoopsSecurityクラス
  • 入力サニタイズ - MyTextSanitizerと検証

SQLインジェクションは、ユーザー入力が適切なサニタイズやパラメータ化なしにSQLクエリに直接含められるときに発生します。

// 危険 - 使用しないでください
$id = $_GET['id'];
$sql = "SELECT * FROM " . $xoopsDB->prefix('items') . " WHERE id = " . $id;
$result = $xoopsDB->query($sql);

ユーザーがIDとして1 OR 1=1を渡す場合、クエリは以下になります:

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はCriteriaシステムを通じてSQLインジェクションを防止するのに役立つオブジェクト指向のデータベースアクセスを提供します。

// ハンドラーを取得
$itemHandler = xoops_getModuleHandler('item', 'mymodule');
// Criteriaを作成
$criteria = new Criteria('category_id', (int)$categoryId);
// オブジェクトを取得 - 自動的にSQLインジェクションから安全
$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);
// 等号(デフォルト)
$criteria->add(new Criteria('status', 'active'));
// 不等号
$criteria->add(new Criteria('status', 'deleted', '!='));
// より大きい
$criteria->add(new Criteria('count', 100, '>'));
// 以下
$criteria->add(new Criteria('price', 50, '<='));
// LIKE(部分一致)
$criteria->add(new Criteria('title', '%' . $searchTerm . '%', 'LIKE'));
// IN(複数値)
$criteria->add(new Criteria('id', '(' . implode(',', $ids) . ')', 'IN'));
$criteria = new CriteriaCompo();
$criteria->add(new Criteria('status', 'published'));
// OR条件
$orCriteria = new CriteriaCompo();
$orCriteria->add(new Criteria('uid', (int)$userId), 'OR');
$orCriteria->add(new Criteria('is_public', 1), 'OR');
$criteria->add($orCriteria);

常にXOOPSテーブルプリフィックスシステムを使用してください:

// 正しい - プリフィックスを使用
$table = $xoopsDB->prefix('mytable');
$sql = "SELECT * FROM {$table} WHERE id = ?";
// また正しい
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";

パラメータ化されたステートメントを使用

Section titled “パラメータ化されたステートメントを使用”
$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();
}
// 新しいオブジェクトを作成
$item = $itemHandler->create();
// 値を設定 - ハンドラーが自動的にエスケープ
$item->setVar('title', $title);
$item->setVar('content', $content);
$item->setVar('uid', (int)$userId);
$item->setVar('created', time());
// 挿入
if ($itemHandler->insert($item)) {
$newId = $item->getVar('itemid');
}

パラメータ化されたステートメント

Section titled “パラメータ化されたステートメント”
$sql = "UPDATE " . $xoopsDB->prefix('mytable') .
" SET title = ?, content = ?, updated = ? WHERE id = ?";
$result = $xoopsDB->query($sql, [
$title,
$content,
time(),
(int)$id
]);
// 既存のオブジェクトを取得
$item = $itemHandler->get((int)$id);
if ($item) {
$item->setVar('title', $title);
$item->setVar('content', $content);
$item->setVar('updated', time());
$itemHandler->insert($item);
}

パラメータ化されたステートメント

Section titled “パラメータ化されたステートメント”
$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);

パラメータ化されたステートメントが使用できない場合は、適切なエスケープを使用してください:

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

ただし、常にパラメータ化されたステートメントをエスケープより優先してください。

列名はパラメータ化できません。ホワイトリストに対して検証してください:

$allowed_columns = ['title', 'created', 'updated', 'status'];
$sort = $_GET['sort'] ?? 'created';
if (!in_array($sort, $allowed_columns)) {
$sort = 'created'; // デフォルトの安全な値
}
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') .
" ORDER BY {$sort} DESC";

同様にテーブル名を検証してください:

$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();
// 入力に基づいて条件を追加
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);

ワイルドカード注入を避けるためにLIKEクエリに注意してください:

// 検索語の特殊文字をエスケープ
$searchTerm = str_replace(['%', '_'], ['\%', '\_'], $searchTerm);
// その後LIKEで使用
$criteria->add(new Criteria('title', '%' . $searchTerm . '%', 'LIKE'));

IN句を使用する場合は、すべての値が適切に型指定されていることを確認してください:

// ユーザー入力からのIDの配列
$inputIds = $_POST['ids'] ?? [];
// サニタイズ: すべてが整数であることを確認
$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);
}

またはCriteriaで:

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

複数の関連クエリを実行する場合:

// トランザクションを開始
$xoopsDB->query("START TRANSACTION");
try {
// クエリ1
$sql1 = "INSERT INTO " . $xoopsDB->prefix('items') . " (title) VALUES (?)";
$result1 = $xoopsDB->query($sql1, [$title]);
if (!$result1) {
throw new Exception('挿入に失敗しました');
}
$itemId = $xoopsDB->getInsertId();
// クエリ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('メタ挿入に失敗しました');
}
// コミット
$xoopsDB->query("COMMIT");
} catch (Exception $e) {
// エラー時はロールバック
$xoopsDB->query("ROLLBACK");
throw $e;
}

SQLエラーをユーザーに公開しないでください:

$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$id]);
if (!$result) {
// 実際のエラーをログに記録(デバッグ用)
error_log('データベースエラー: ' . $xoopsDB->error());
// ユーザーに一般的なメッセージを表示
redirect_header('index.php', 3, 'エラーが発生しました。もう一度試してください。');
exit();
}
// 悪い
$sql = "SELECT * FROM {$table} WHERE id = {$id}";
// 良い
$sql = "SELECT * FROM " . $xoopsDB->prefix('mytable') . " WHERE id = ?";
$result = $xoopsDB->query($sql, [(int)$id]);
// 悪い - addslashesは不十分
$safe = addslashes($_GET['input']);
// 良い - パラメータ化クエリまたは適切なエスケープを使用
$sql = "SELECT * FROM table WHERE col = ?";
$result = $xoopsDB->query($sql, [$_GET['input']]);
// 悪い - 入力が数値と仮定
$id = $_GET['id'];
$sql = "SELECT * FROM table WHERE id = " . $id;
// 良い - 明示的に整数にキャスト
$id = (int)$_GET['id'];
$sql = "SELECT * FROM table WHERE id = ?";
$result = $xoopsDB->query($sql, [$id]);

間違い4: セカンドオーダーインジェクション

Section titled “間違い4: セカンドオーダーインジェクション”
// データベースからのデータは自動的に安全ではありません
$userData = $itemHandler->get($id);
$username = $userData->getVar('username');
// 悪い - データベースからのデータを信頼
$sql = "SELECT * FROM log WHERE username = '" . $username . "'";
// 良い - 常にパラメータを使用
$sql = "SELECT * FROM log WHERE username = ?";
$result = $xoopsDB->query($sql, [$username]);

SQLインジェクションをチェックするために、これらの入力でフォームをテストしてください:

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

これらのいずれかが予期しない動作やエラーを引き起こした場合、脆弱性があります。

開発中に自動SQLインジェクションテストツールを使用してください:

  • SQLMap
  • Burp Suite
  • OWASP ZAP
  1. 常にパラメータ化クエリを使用 (準備されたステートメント)
  2. 可能な場合はXoopsObject/XoopsObjectHandlerを使用
  3. Criteriaクラスを使用してクエリを構築
  4. 列とテーブル名をホワイトリストに対して検証
  5. 数値値を明示的に型キャストする (int)または(float)
  6. データベースエラーをユーザーに公開しない
  7. 複数の関連クエリにはトランザクションを使用
  8. 開発中にSQLインジェクションをテスト
  9. 検索クエリのLIKEワイルドカードをエスケープ
  10. IN句の値を個別にサニタイズ

#セキュリティ #sqlインジェクション #データベース #xoops #準備されたステートメント #Criteria