@nqounetです。
前回はパラメータ地獄を体験しました。今回はそれよりも深刻な問題、セキュリティホールについて見ていきます。ちょっとダークサイドを覗いてみましょう(教育目的ですよ!)。
倫理的配慮と免責事項
[!CAUTION]
本記事で扱うSQLインジェクション実験は、セキュリティ学習を目的とした教育的コンテンツです。
- 実験は必ず自分のローカル環境(Docker、SQLite等)で行ってください
- 実際のWebサイトへの攻撃は犯罪行為です(不正アクセス禁止法違反)
- 脆弱性を発見した場合は責任ある開示(Responsible Disclosure)を行ってください
- 本記事の内容を悪用した場合、一切の責任を負いません
前回のコードを振り返る
前回作成したQuery.pmを見てみましょう:
1
2
3
| if ($self->where_column && defined $self->where_value) {
$sql .= " WHERE " . $self->where_column . " = '" . $self->where_value . "'";
}
|
where_valueをそのまま文字列結合しています。これが問題です。
攻撃してみる
ログイン画面を想像してください。ユーザーが入力した値で認証を行うシステムです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, DBI, DBD::SQLite
use v5.36;
use lib 'lib';
use Query;
use DBI;
# テスト用データベース
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
RaiseError => 1,
});
$dbh->do('CREATE TABLE users (id INTEGER, username TEXT, password TEXT)');
$dbh->do("INSERT INTO users VALUES (1, 'admin', 'secret123')");
$dbh->do("INSERT INTO users VALUES (2, 'alice', 'password1')");
$dbh->do("INSERT INTO users VALUES (3, 'bob', 'password2')");
# 正常な検索
say "=== 正常なクエリ ===";
my $normal_query = Query->new(
table => 'users',
where_column => 'username',
where_value => 'admin',
);
say $normal_query->to_sql;
# 出力: SELECT * FROM users WHERE username = 'admin'
|
これは正常に動作します。adminユーザーの情報だけが取得されます。
悪意ある入力
では、ユーザー入力として以下の値が渡されたらどうなるでしょう?
1
2
3
4
5
6
7
8
| # 攻撃クエリ
say "\n=== 攻撃クエリ ===";
my $attack_query = Query->new(
table => 'users',
where_column => 'username',
where_value => "' OR '1'='1", # 悪意ある入力
);
say $attack_query->to_sql;
|
出力されるSQLを見てみましょう:
1
| SELECT * FROM users WHERE username = '' OR '1'='1'
|
'1'='1'は常にTRUEです。つまり、このクエリはテーブルの全レコードを返します。
実行結果を確認
1
2
3
4
5
6
7
| my $sql = $attack_query->to_sql;
my $rows = $dbh->selectall_arrayref($sql, { Slice => {} });
say "取得件数: " . scalar($rows->@*);
for my $row ($rows->@*) {
say "ID: $row->{id}, Username: $row->{username}, Password: $row->{password}";
}
|
結果:
1
2
3
4
| 取得件数: 3
ID: 1, Username: admin, Password: secret123
ID: 2, Username: alice, Password: password1
ID: 3, Username: bob, Password: password2
|
パスワードを含む全ユーザーの情報が漏洩しました。
なぜ起きるのか
問題の根本原因は、ユーザー入力をそのままSQLに埋め込んでいることです。
1
2
| # 危険なコード
$sql .= " WHERE " . $column . " = '" . $value . "'";
|
この実装では、$valueに含まれるシングルクォートがSQLの構文として解釈されてしまいます。
1
2
3
4
5
6
| 入力: ' OR '1'='1
↓
SQL: WHERE username = '' OR '1'='1'
─────────────────┬─────────────
│
攻撃者が注入したSQL
|
さらに危険な攻撃
データを破壊する攻撃も可能です:
1
2
3
4
5
6
7
| my $destroy_query = Query->new(
table => 'users',
where_column => 'username',
where_value => "'; DROP TABLE users; --",
);
say $destroy_query->to_sql;
# 出力: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
|
--はSQLのコメントです。これによりDROP TABLEが実行され、データが消失する可能性があります。
今回のまとめ
今回はSQLインジェクションの危険性を実験で確認しました。
- 文字列結合によるSQL生成は危険
- ユーザー入力をそのまま埋め込むと攻撃の標的になる
- 全データ漏洩、データ破壊などの被害が発生する
この問題を解決するには、2つのアプローチがあります:
- Builderパターンで構築プロセスを制御し、安全なSQLを生成する
- プレースホルダー(バインドパラメータ)で値を分離する
次回は、Builderパターンを導入して、パラメータ地獄とセキュリティ問題を同時に解決する方法を学びます。