Featured image of post 第3回 - SQLインジェクション実験(Perl セキュリティ入門)

第3回 - SQLインジェクション実験(Perl セキュリティ入門)

SQLクエリビルダー第3回。文字列結合でSQLを組み立てる危険性を、SQLインジェクション実験で体験。セキュリティ意識を高める教育的コンテンツ。

@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つのアプローチがあります:

  1. Builderパターンで構築プロセスを制御し、安全なSQLを生成する
  2. プレースホルダー(バインドパラメータ)で値を分離する

次回は、Builderパターンを導入して、パラメータ地獄とセキュリティ問題を同時に解決する方法を学びます。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。