はじめに
今回はWebアプリケーションのセキュリティ検証ツールであるBurp Suiteを使用して、SQLインジェクション脆弱性の検証方法と、その防御策の重要性について解説します。
そこまで深い話はしていませんが、本記事は防御目的です。悪用は厳禁です。
準備
1. Burp Suiteの導入
下記リンクからBurp Suiteをダウンロードする。
(メールアドレスの入力が必要です)
https://portswigger.net/burp/communitydownload
無償のCommunity Edition を選択します。
OSに関しては、ご自身の環境に合ったものを選択してください。
2. テスト用サイトの準備
フロント: React
バックエンド: Express
DB: PostgreSQL
で構築
(参考: https://qiita.com/cho-tehu/items/371802e4c0bf0f7d29cc)
import VulnerableSearch from './components/VulnerableSearch'
import SafeSearch from './components/SafeSearch'
import VulnerablePullDown from './components/VulnerablePullDown'
import SafePullDown from './components/SafePullDown'
export default function App() {
return (
<div style={{ padding: 20 }}>
<h1>Burp Test Frontend</h1>
<section>
<h2>SQLi (Item lookup)</h2>
<VulnerablePullDown />
<SafePullDown />
</section>
</div>
)
}
import React, { useState } from 'react'
import axios from 'axios'
interface Item {
id: number
name: string
}
const VulnerablePullDown: React.FC = () => {
const [query, setQuery] = useState<string>('1')
const [result, setResult] = useState<Item[]>([])
const search = async () => {
try {
// 脆弱:文字列連結で SQL に渡す(SQLi 再現用)
const r = await axios.get<{ rows: any }>(
`http://localhost:4000/vuln/item?id=${query}`,
{ withCredentials: true }
)
console.log(r.data.rows.rows)
setResult(r.data.rows.rows)
} catch (e) {
console.error(e)
setResult([])
}
}
return (
<div style={{ border: '1px solid #f99', padding: 8, marginBottom: 8 }}>
<h3>Vulnerable PullDown (SQLi)</h3>
<select value={query} onChange={e => setQuery(e.target.value)} style={{ marginRight: 8 }}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={search}>Search</button>
<pre style={{ marginTop: 8 }}>{JSON.stringify(result, null, 2)}</pre>
</div>
)
}
export default VulnerablePullDown
import React, { useState } from 'react'
import axios from 'axios'
interface Item {
id: number
name: string
}
const SafePullDown: React.FC = () => {
const [query, setQuery] = useState<string>('1')
const [result, setResult] = useState<Item[]>([])
const search = async () => {
try {
// 安全:パラメータ化クエリをサーバ側で処理している前提
const r = await axios.get<{ rows: Item[] }>(
`http://localhost:4000/safe/item?id=${query}`,
{ withCredentials: true }
)
setResult(r.data.rows)
} catch (e) {
console.error(e)
setResult([])
}
}
return (
<div style={{ border: '1px solid #9f9', padding: 8, marginBottom: 8 }}>
<h3>Safe PullDown (param query)</h3>
<select value={query} onChange={e => setQuery(e.target.value)} style={{ marginRight: 8 }}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={search}>Search</button>
<pre style={{ marginTop: 8 }}>{JSON.stringify(result, null, 2)}</pre>
</div>
)
}
export default SafePullDown
import express, { type Request, type Response } from 'express';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import csrf from 'csurf';
import cors from 'cors';
import { Pool } from 'pg';
// -----------------------
// DB Pool(グローバルに 1 回だけ作成)
const db = new Pool({
host: "db",
port: 5432,
user: "user",
password: "pass",
database: "appdb",
});
// -----------------------
// ヘルパー: クエリ実行
async function query(text: string, params: any[] | undefined) {
const client = await db.connect();
try {
const res = await client.query(text, params);
return res;
} finally {
client.release();
}
}
// -----------------------
const app = express();
app.use(cors({ origin: 'http://localhost:5173', credentials: true }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cookieParser());
// CSRF 用
const csrfProtection = csrf({ cookie: true });
// -----------------------
// 脆弱:SQLi を示すエンドポイント
// GET /vuln/item?id=1
app.get('/vuln/item', async (req: Request, res: Response) => {
const id = req.query.id || '';
// 危険:直接文字列連結(SQLi の例)
const sql = `SELECT * FROM items WHERE id = ${id}`;
try {
const r = await query(sql, undefined);
console.log(r)
res.json({ rows: r });
} catch (err) {
console.log(err)
res.status(500).send(err instanceof Error ? err.message : String(err));
}
});
// 安全:パラメータ化クエリ
// GET /safe/item?id=1
app.get('/safe/item', async (req: Request, res: Response) => {
const id = req.query.id || '';
try {
const r = await query('SELECT * FROM items WHERE id = $1', [id]);
res.json({ rows: r.rows });
} catch (err) {
console.log(err)
res.status(500).send('internal error');
}
});
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`backend listening on ${PORT}`);
});
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
balance INTEGER NOT NULL DEFAULT 0
);
-- ------------------------
-- items テーブル
-- ------------------------
INSERT INTO items (id, name) VALUES (1, 'Item A') ON CONFLICT (id) DO NOTHING;
INSERT INTO items (id, name) VALUES (2, 'Item B') ON CONFLICT (id) DO NOTHING;
INSERT INTO items (id, name) VALUES (3, 'Item C') ON CONFLICT (id) DO NOTHING;
-- ------------------------
-- users テーブル
-- ------------------------
INSERT INTO users (id, username, balance) VALUES (1, 'alice', 1000) ON CONFLICT (id) DO NOTHING;
INSERT INTO users (id, username, balance) VALUES (2, 'bob', 500) ON CONFLICT (id) DO NOTHING;
INSERT INTO users (id, username, balance) VALUES (3, 'carol', 2000) ON CONFLICT (id) DO NOTHING;
-- 2. items
SELECT setval('items_id_seq', COALESCE((SELECT MAX(id) FROM items), 0) + 1, false);
-- 3. users
SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0) + 1, false);
テスト実施
Burp Suiteを開き、Proxy > Intercept > Open Browser
Open Browserを押すと、Chromium(簡単に言うとChromeのオープンソース版)が開く
アドレスバーに検証するサイトのアドレスを入力して、開く
Intercept on にする。
こうすることでリクエストをキャプチャして、送る前に止めることができる。
Chromiumで開いている検証対象サイトで開発者ツールのNetWorkを開く。
リクエスト(今回はVulnerable PullDown で検証)を送ると、pendingの状態で止まる。
Burp Suite側でリクエストを見つけ、クリックすると、リクエストを編集できる画面が開く。
下記のように書き換えてみる。
※ %20 はURLエンコーディングで半角スペースを表す
1;%20SELECT%20*%20FROM%20users
対象のリクエストを右クリック、Forwardを選択し、リクエストを送信する。
すると、Responseでユーザ一覧が取得できてしまう。
(サーバ側でSQL文字列を結合しているため、ユーザ入力がSQL構文として実行されてしまう)
まとめ:防御のために必要なこと
上記の検証から、以下のことに注意する必要があることが分かります。
- 自サイトのフロントからのみAPIが叩ける設定にしていても関係ない
- ユーザ入力の値をクエリに組み込む際は、プルダウンなど入力可能な文字を制御している場合でも、必ずサニタイズを行う
- DB操作結果をバックエンドから返す際は、必ず最小限の内容を返す(rowsのみを返す。型定義を行い、想定する型になっているかバックエンド側でチェックを行う等)
最後に
このようにBurp Suiteを使うとリクエストの改ざんが簡単にできます。
プルダウンや、フロントで入力チェックを行っている場合も余裕で改ざんできるので、安心できません。
自サイトのフロントからのみAPIが叩ける設定にしていても、かなり自由にリクエストを編集できてしまうので、注意が必要です。
professional版にすると自動で脆弱性スキャンができるそうなので、ぜひ活用してみてください。
私もそこまで詳しくないので、こういう使い方もあるよ等あれば教えていただきたいです。