個人開発のWebサービスがSQLインジェクション攻撃を受けた話と対策まとめ

個人開発のWebサービスがSQLインジェクション攻撃を受けた話と対策まとめ

まとめ系

個人開発で運営しているWebサービスが攻撃を受けたので、その内容と対策をまとめました。

攻撃を受けた経緯

ある日、サーバーのログを確認していたところ、不審なリクエストを発見しました。

お問い合わせフォームの「姓」フィールドに、以下のような文字列が入力されていたのです。


-1" OR 5*5=25 --

これはSQLインジェクション攻撃と呼ばれるもので、データベースを不正に操作しようとする攻撃手法です。

SQLインジェクションとは

SQLインジェクションは、入力フィールドに悪意のあるSQL文を注入して、データベースを操作しようとする攻撃です。

例えば、ログイン処理で以下のようなSQLを使っていたとします。

sql
SELECT * FROM users WHERE name = '入力値'

ここに ' OR '1'='1 のような文字列を入力されると、

sql
SELECT * FROM users WHERE name = '' OR '1'='1'

となり、条件が常にTRUEになってしまいます。これにより、認証をバイパスされたり、データを不正に取得されたりする可能性があります。

今回の攻撃で使われた -1" OR 5*5=25 -- も同様の原理で、5*5=25 は常にTRUEになるため、データベースの挙動を変えようとしていたものと思われます。

攻撃者の情報を分析してみた

ログに残っていた情報から、攻撃者について分析してみました。

| 項目 | 内容 |
|——|——|
| IPアドレス | VPNプロバイダーの帯域 |
| ブラウザ | Chrome(Windows) |
| メールアドレス | sample@email.tst(明らかにダミー) |
| 入力パターン | 最小限の値 + 攻撃コード |

攻撃の特徴から分かったこと

いくつかの特徴から、この攻撃について考察してみました。

1. 汎用的なペイロードが使われていた

使われていた攻撃コードは非常に一般的なもので、特定のサービスを狙ったものではありませんでした。サイトのDB構造を知っているような痕跡もなく、「とりあえず試してみた」という印象です。

2. 入力が機械的だった

名前欄に「e」、メールに「sample@email.tst」など、明らかに自動化ツールで生成したような値が使われていました。SQLMap等の脆弱性スキャンツールの特徴に似ています。

3. VPN経由でアクセスしていた

IPアドレスを調べると、VPNプロバイダーがよく使用する帯域でした。身元を隠しながら多くのサイトをスキャンする際の典型的な手法です。

結論:無差別スキャンの可能性が高い

これらの特徴から、特定のサービスを狙った標的型攻撃ではなく、脆弱なサイトを探すボットによる無差別スキャンだったと判断しました。

個人開発の小規模なサービスでも、こういった攻撃は普通に来るんだなと実感しました。

実施した対策

今回の攻撃を受けて、以下の対策を実施しました。

1. IPブラックリストの実装

攻撃元のIPアドレスを即座にブロックする仕組みを追加しました。

typescript
// IPブラックリスト
const IP_BLACKLIST: Set<string> = new Set([
'178.16.55.xxx', // 攻撃元IP
])

export function checkIpBlacklist(event: H3Event): void {
const ip = getClientIp(event)

if (IP_BLACKLIST.has(ip)) {
console.log(
[BLOCKED] Blacklisted IP: ${ip})
throw createError({
statusCode: 403,
message: 'Access denied'
})
}
}

シンプルな実装ですが、同じIPからの継続的な攻撃には効果的です。

2. 既存の対策が機能しているか確認

改めて既存のセキュリティ対策を確認しました。

| 対策 | 状態 | 説明 |
|——|——|——|
| パラメータ化クエリ | ✅ | ORMを使用しており、SQLインジェクション対策済み |
| レート制限 | ✅ | 1分間に数回までに制限 |
| reCAPTCHA v3 | ✅ | ボット判定を実施 |
| 入力長制限 | ✅ | 追加で実装 |

重要なのは、パラメータ化クエリを使用していたため、今回の攻撃は実際には成功していなかったという点です。

Supabaseのクライアントライブラリを使っていたので、入力値は自動的にエスケープされており、SQLインジェクションは成立しませんでした。

typescript
// Supabaseはパラメータ化クエリを使用するため安全
const { error } = await client
.from('inquiries')
.insert({
last_name, // 値は安全にエスケープされる
email,
message,
})

3. 入力値の長さ制限を追加

説明欄などに大量のテキストを送りつけられるのを防ぐため、文字数制限を追加しました。

typescript
const MAX_DESCRIPTION_LENGTH = 1000

if (description && description.length > MAX_DESCRIPTION_LENGTH) {
throw createError({
statusCode: 400,
message:
説明は${MAX_DESCRIPTION_LENGTH}文字以内で入力してください
})
}

reCAPTCHA v3がボットを検出できる仕組み

せっかくなので、reCAPTCHA v3がどうやってボットを検出しているのかも調べてみました。

v3は「私はロボットではありません」のチェックボックスがなく、ユーザーの**行動パターン**を分析してスコア(0.0〜1.0)を算出します。

| 分析要素 | 人間の特徴 | ボットの特徴 |
|———-|————|————–|
| マウスの動き | 曲線的、不規則 | 直線的、または動きなし |
| キー入力 | タイミングにばらつき | 均一で高速 |
| ページ滞在時間 | 入力に時間がかかる | 瞬時に送信 |
| スクロール | 自然な速度変化 | 一定または無し |

スコアが閾値(例: 0.5)を下回ると、ボットと判定されてブロックされます。自動化ツールはこういった「人間らしさ」を再現するのが難しいようです。

多層防御の考え方

今回の経験で改めて実感したのは、多層防御の重要性です。

一つの対策に頼るのではなく、複数の防御層を設けることで、どれか一つが突破されても他の層で防げるようになります。


[攻撃]
→ IPブラックリスト(既知の攻撃元をブロック)
→ レート制限(大量リクエストを防止)
→ reCAPTCHA(ボットを検出)
→ 入力検証(不正な値を拒否)
→ パラメータ化クエリ(SQLインジェクションを無効化)

今回は最後の砦である「パラメータ化クエリ」で攻撃が無効化されていましたが、他の層も機能していたことで、より安心できる状態でした。

学んだこと

今回の攻撃を通じて、いくつかのことを学びました。

1. 基本的な対策が重要

ORMやクエリビルダーを正しく使っていれば、SQLインジェクションは基本的に防げます。フレームワークの機能をちゃんと使うことが大切だなと思いました。

2. ログは必ず残す

攻撃を検知できたのは、リクエストのログを残していたからです。以下の情報は記録しておくと、分析に役立ちます。

– IPアドレス
– User-Agent
– リクエスト内容(個人情報の取り扱いには注意)
– タイムスタンプ

3. 小規模サービスでも攻撃は来る

「うちは小さいから大丈夫」は通用しないんだなと実感しました。ボットは無差別にスキャンしているので、サービスの規模は関係ありません。

4. 定期的なログ確認の習慣

普段からログを確認する習慣をつけておくと、異常に気づきやすくなります。今回も定期的なログ確認で発見できました。

まとめ

今回の攻撃は、おそらく脆弱なサイトを探す自動スキャンでした。幸い対策済みだったため実害はありませんでしたが、個人開発でも普通に攻撃が来るんだなという良い経験になりました。

Webサービスを運営している方は、ぜひ以下の点を確認してみてください。

– SQLインジェクション対策ができているか(パラメータ化クエリの使用)
– ログを確認する習慣があるか
– 多層防御を意識した設計になっているか

何かの参考になれば幸いです。