セキュリティについて学んでみた【SQLインジェクション編】
SQLインジェクションとは
SQLクエリの構造に意図的にコードを挿入することで、データベースに対して不正な操作を行う攻撃手法です。
SQLインジェクション攻撃の例
bunのSQLite3ドライバーを使用し、Honoでlocalhost:3000
(被害側)とlocalhost:4000
(攻撃側)にアクセスできるようにします。
被害側
/user
エンドポイントで、URLのクエリパラメータとして指定されたidを基にusers
テーブルからデータを取得する処理を行います。
users
テーブルには、適当にレコードを作成しておきます。
import { Hono } from "hono";
import { Database } from "bun:sqlite";
const app = new Hono();
const db = new Database(":memory:");
db.run(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, role TEXT)"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('admin', 'adminpass', 'admin')"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('john_doe', 'john123', 'user')"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('jane_doe', 'jane123', 'user')"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('alice', 'alice123', 'user')"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('bob', 'bob123', 'user')"
);
db.run(
"INSERT INTO users (username, password, role) VALUES ('charlie', 'charlie123', 'user')"
);
app.get("/user", (c) => {
const id = c.req.query("id");
const query = db.query(`SELECT * FROM users WHERE id = ${id}`);
const user = query.all();
return c.json(user);
});
export default app;
攻撃側
/attack
というエンドポイントを作成し、SQLインジェクションを試みるリクエストをlocalhost:3000の/user
エンドポイントに送信します。
import { Hono } from "hono";
const app = new Hono();
app.get("/attack", async (c) => {
const response = await fetch("http://localhost:3000/user?id=1 OR 1=1");
const data = await response.json();
return c.json(data);
});
export default {
port: 4000,
fetch: app.fetch,
};
攻撃開始
http://localhost:4000/attack
にアクセスすると、以下のようなjsonが返されます。
[{"id":1,"username":"admin","password":"adminpass","role":"admin"},{"id":2,"username":"john_doe","password":"john123","role":"user"},{"id":3,"username":"jane_doe","password":"jane123","role":"user"},{"id":4,"username":"alice","password":"alice123","role":"user"},{"id":5,"username":"bob","password":"bob123","role":"user"},{"id":6,"username":"charlie","password":"charlie123","role":"user"}]
これは、被害側でSELECT * FROM users WHERE id=1 OR 1=1
が実行されるためです。
OR 1=1 の条件があると、次のように解釈されます。
- id = 1 または 1=1 のどちらかの条件を満たせば、その行がクエリの結果に含まれます。
- 1=1 は常に真なので、users テーブル内のすべての行がクエリ結果に含まれます。
したがって、users テーブル内のすべてのデータが返されることになります。
SQLインジェクションの対策
SQLインジェクションに対する対策の方法は パラメータ化クエリを使用することです。これにより、ユーザー入力が直接SQL構文に挿入されず、データベースが入力を単なるデータとして扱うため、インジェクションが無効化されます。
被害側コードの修正
被害側の/user
エンドポイントのコードを以下のように修正します。
app.get("/user", (c) => {
const id = c.req.query("id");
const query = db.query(`SELECT * FROM users WHERE id = ?`);
const user = query.all(id);
return c.json(user);
});
これにより攻撃側では、空のjsonが返されます。
パラメータ化クエリでは防げないパターン
テーブル名やカラム名をユーザーの入力で変更する場合、パラメータ化クエリで保護できません。このため、思わぬデータへのアクセスが許される可能性があります。
テーブル名を動的にした場合
table=sqlite_master --
を送信することで、データベースのスキーマ情報が取得されます。sqlite_master
テーブルはSQLiteのメタデータを保持しています。
app.get("/select_table", (c) => {
const table = c.req.query("table");
const query = db.query(`SELECT * FROM ${table} WHERE role = ?`);
const results = query.all("user");
return c.json({
message: `Data from table ${table}`,
data: results,
});
});
app.get("/attack_table", async (c) => {
const targetUrl = "http://localhost:3000/select_table?table=sqlite_master --";
const response = await fetch(targetUrl);
const result = await response.json();
return c.json(result);
});
対策
テーブル名はパラメータ化できないため、ホワイトリストで検証することでSQLインジェクションを防止します。
app.get("/select_table", (c) => {
const table = c.req.query("table");
// ホワイトリストを使用してテーブル名を検証
const allowedTables = ["users"];
if (!allowedTables.includes(table)) {
return c.json({ message: "Invalid table name" }, 400);
}
const query = db.query(
`SELECT id, username, role FROM ${table} WHERE role = ?`
);
const results = query.all("user");
return c.json({
message: `Data from table ${table}`,
data: results,
});
});
Discussion