説明
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文字列と値を分けて渡します。