🔐

セキュリティについて学んでみた【SQLインジェクション編】

2024/11/07に公開

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