SQLインジェクション

SQLインジェクション

説明

SQLインジェクションは、ユーザー入力をパラメーター化せずにクエリ文字列へ連結すると発生します。入力が値ではなくSQL構文として解釈され、WHERE 条件の変更、UNION、サブクエリ、関数呼び出しなどを挿入されるおそれがあります。引用符、OR、UNION、-- などのコメント構文を組み合わせ、認証を回避したり、データを取得・変更・削除したりできる場合があります。sequelize.query でraw queryに入力を連結する場合も同様です。

想定される影響

  • ログインや権限確認のクエリを操作することによる認証の回避
  • UNION やサブクエリの注入による機密データの漏えい
  • INSERT、UPDATE、DELETE、DROP などによるデータの改ざん・削除・損失
  • 複雑なクエリや SLEEP などの待機関数によるサービス拒否

対処方法

  • 入力をSQL文字列に連結せず、sequelize.query の replacements または bind で値を別に渡してください。replacements はSequelizeが値をエスケープしてクエリに挿入し、bind は値をクエリ文字列と分けてDBに渡します。
  • テーブル名や列名などの識別子には、値のプレースホルダーを使えません。サーバーが定めた許可リストから選んでください。
  • 可能なら Model.findAll や findOne の attributes・where など、構造化されたオプションを使ってください。ORMを使っても、手動で組み立てたraw SQLが安全になるわけではありません。
  • 長さと形式も検証してください。手動のエスケープに依存したり、MySQL用の sqlstring をPostgreSQLのクエリに使ったりしないでください。

例

変更前

javascript
const express = require("express");
const { Sequelize } = require("sequelize");
const app = express();

const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");

// 変更前: ユーザー入力をそのままraw SQLに連結します。
app.get("/items", async (req, res) => {
  const col = req.query.col; // 例: name
  const term = req.query.term; // 例: a'
  const sql = `SELECT ${col} FROM products WHERE name LIKE '%${term}%'`;
  try {
    const [rows] = await sequelize.query(sql);
    res.json(rows);
  } catch (e) {
    res.status(500).send("error");
  }
});

変更後

javascript
const express = require("express");
const { Sequelize, QueryTypes } = require("sequelize");
const app = express();

const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");

// 変更後: 値はreplacements、識別子は許可リストで扱います。
app.get("/items", async (req, res) => {
  const allowedCols = new Set(["name", "price", "created_at"]);
  const requested = String(req.query.col || "name");
  if (!allowedCols.has(requested)) {
    return res.status(400).send("invalid column");
  }

  const term = String(req.query.term || "");
  // 値はSQL文字列に連結せず、別に渡します。
  const sql = `SELECT ${requested} FROM products WHERE name ILIKE ?`;
  try {
    const rows = await sequelize.query(sql, {
      replacements: [`%${term}%`],
      type: QueryTypes.SELECT,
    });
    res.json(rows);
  } catch (e) {
    res.status(500).send("error");
  }
});

// 代替案: ORM APIの構造化されたクエリオプションを使います。
// Product.findAll({ attributes: [requested], where: { name: { [Op.iLike]: `%${term}%` } } });

説明:

  • 変更前: term と col をSQLに直接挿入するため、クエリの条件や構造を変更されるおそれがあります。
  • 変更後: 値は replacements で処理し、列名は固定の許可リストからだけ選びます。replacements とDBのバインディングは仕組みが異なりますが、どちらもアプリからAPIへSQL文字列と値を分けて渡します。

参考資料