SQLインジェクション対策の基礎から実践まで:プレースホルダ・エスケープ処理をわかりやすく解説

この記事の対象読者

この記事は、大切な企業資産を守りたい経営層の方や情報システム・ITセキュリティの担当者の方を対象に、SQLインジェクションについて前提となる技術の解説から具体的な対策方法などの専門的な知識まで網羅的かつわかりやすく解説します。SQLインジェクションを理解することは、ビジネスにおける重要なセキュリティ対策の一環です。

目次

SQLインジェクションを理解するための前提知識

SQLインジェクションについて詳しく解説する前に、まずはデータベースとSQLという前提となる技術について、具体例を交えてわかりやすく解説します。これらの知識に既に精通されている方は、SQLインジェクションについて知っておきたいことに直接進んでいただいても構いません。

データベースとは

データベースとは、その名の通り情報(データ)を保管する場所(ベース)のことです。例えば、オンラインショップなどのWebサービスでは、顧客や商品に関する情報をデータベースに保存しておき、実際に注文が入った時に必要な情報をデータベースから取り出して、決済や出荷の対応を行います。データベースの中に保存されている情報を検索したり、新しいデータを追加したりするためのシステムをDBMS(Database Management System:データベース管理システム)と呼びます。一般的には、データベースとDBMSは同じ意味で使うことが多いです。

DBMSの種類・特徴

DBMSは、主に「リレーショナルデータベース」と「非リレーショナルデータベース」の2種類に大別されます。

リレーショナルデータベース(RDBMS)は、Microsoft ExcelやGoogleスプレッドシートのような表形式でデータを管理するDBMSのことを指します。このリレーショナルデータベースでは、後述するSQL(Structured Query Language)を使用してデータの取得や追加、削除などの操作を行います。代表的なRDBMSとして、MySQLやOracle Database、Microsoft SQL Serverなどがあります。

一方、非リレーショナルデータベース(Non-Relational DBMS)は、別名NoSQL(Not Only SQL)データベースとも呼ばれ、リレーショナルデータベースとは異なる形式でデータを管理するDBMSの総称です。代表的なものとしては、MongoDB、Cassandra、Redis、Elasticsearchなどがあります。

リレーショナルデータベースは、データ間の関連性を強調し、構造化されたデータを効率的に管理できるという特徴があります。一方、非リレーショナルデータベースは、柔軟性とスケーラビリティ(拡張性)に優れ、大規模なデータや変動の激しいデータを管理することに適しています。企業がDBMSを選択する際には、扱うデータの性質や利用目的に合わせて検討することが重要です。

この記事では、これ以降のDBMSに関する記述はRDBMSを扱うものとします。

テーブルとは

テーブルとは、データを種類ごとに分類し、関連するものをまとめて管理するための表のことを指します。下図の例では、1つのDBMSの中に3つのデータベース(shopstaffaccount)が管理されています:

3つのデータベースのうち、あるオンラインショップに関するデータベースshopに関して見てみましょう。このshopには、顧客のメールアドレスやログインパスワード、住所などの連絡先を管理するusersテーブルや、商品情報(商品名や商品説明、在庫数など)を管理するproductsテーブル、注文情報(受付日や合計額、支払い方法、決済完了の有無など)を管理するordersテーブルがあります。同じグループの情報をテーブルごとに管理することによって、必要な時に必要な情報にスムーズにアクセスできるようにしています。

テーブルの構造

テーブルは、Microsoft ExcelやGoogleスプレッドシートのように横に並ぶ「行」と、縦に配置される「列」で構成されています。新しいデータをテーブルに追加すると新しい行(レコードという)が追加され、データを削除すると行が削除される仕組みです。一方で列には、データの属性(どのようなデータか)を示すカラム(Column)があり、テーブルごとに固有の名前を付けます。下図の例では、idusernamepasswordという属性(カラム)があり、その表形式に従ってレコード(データ)が保存されています:

一般的にカラムには、データの種類(データ型という)を設定する決まりがあります。代表的には、数値型、文字列型(テキスト型)、日付型などがあります。一部のDBMSには地理空間といった複雑なデータ型を扱うものもあります。なぜあらかじめデータ型を設定する必要があるかというと、誤って意図しないデータが保存されないようにするためです。例えば、本来は日付型のデータを保存するためのところに誤って文字列型のデータが格納されようとするとDBMSによってエラーが通知されます。こうした仕組みによりデータの整合性を担保しています。

それでは、次はデータの取得や追加、更新、削除する方法を見ていきましょう。これらの操作を行う(DBMSに指示する)ために必要なのがプログラミング言語「SQL」です。

SQLとは

SQL(Structured Query Language)とは、リレーショナルデータベース(RDBMS)を操作するために用いるISO(International Organization for Standardization:国際標準化機構)によって標準化されているプログラミング言語です。SQLは多くのRDBMSに用いられており、決まった文法に従って命令文(クエリという)を記述します。代表的なクエリには以下のものがあります:

  • SELECT(データの検索):特定の条件に基づいてデータベースからデータを取得する
  • INSERT(データの挿入):新しいデータをデータベースに追加する
  • UPDATE(データの更新):データベースに格納されている既存データを変更する
  • DELETE(データの削除):不要なデータをデータベースから削除する

SELECT

SELECTは、特定の条件に基づいてデータベースからデータを取得するためのクエリで、最も基本的な記述は次のようになります:

SELECT * FROM users;

最初の単語SELECTは、データを取得したいことをDBMSに伝えるものです。その横にあるアスタリスク*は「全て」という意味を持つ記号で、下記の表の例においては、idusernamepasswordの3つ全てのカラムのデータを取得したいことをDBMSに伝えています。その後に続くFROM usersという記述がusersテーブルからデータを取り出すことを指示し、最後にセミコロン;を付けることでこのクエリが終わりであることをデータベースに明示します。このクエリによって取得できるデータは下記の表のようになります:

idusernamepassword
1田中太郎pass123
2山田次郎456word
3鈴木三郎secret789
4adminp4ssw0rd

次に、上記のクエリの例で用いられていたアスタリスク*の代わりに、具体的なカラムを2つ指定したクエリの例を見てみましょう:

SELECT username, password FROM users;

ここでは「全て」という意味を持つアスタリスク*の代わりにusernamepasswordという2つのカラムを指定しているため、このクエリによって取得できるデータは下記の表のようになります(上記の表と比較して、今回はidに関するデータが取得されていないことがポイントです):

usernamepassword
田中太郎pass123
山田次郎456word
鈴木三郎secret789
adminp4ssw0rd

また、カラム名を指定するだけではなく、検索したいデータの条件を指定することも可能です。その場合はWHEREという語句を使います。例えば、カラムusernameadminという文字列と一致する行(レコード)を検索したい場合、次のようなクエリになります:

SELECT * FROM users WHERE username = 'admin';

このクエリの結果は下記の表のようになります。「usernameが文字列adminと一致する」という検索条件に合致しているレコードが取得されています(検索条件に合致しているレコード以外は取得されていないことがポイントです):

idusernamepassword
4adminp4ssw0rd

また、検索条件は複数指定することができます。例えば、usernameadminと一致し、かつpasswordp4ssw0rdと一致するレコードを取得したい場合は、次のようなクエリになります:

SELECT * FROM users WHERE username = 'admin' AND password = 'p4ssw0rd';

オンラインショップなどの一般的なWebサービスにあるログイン機能(認証という)の裏側では、このようなクエリによってDBMSが操作されており、入力されたユーザー名とパスワードが合致した場合のみログインが許可される仕組みとなっています。

INSERT

INSERTは、新しいレコードをテーブルに追加するためのクエリです。具体例を見てみましょう:

INSERT INTO users (username, password) VALUES ('木村吾郎', 'kimura560');

最初のINSERT INTO usersが「usersテーブルに新しいデータを追加する」ことをDBMSに伝えています。その後に続く(username, password) VALUES ('木村吾郎', 'kimura560')で各カラムに対してどのような値を格納するかを指定しています。このクエリが実行されると新しいデータが追加されます(表の最後に1行追加されています):

idusernamepassword
1田中太郎pass123
2山田次郎456word
3鈴木三郎secret789
4adminp4ssw0rd
5木村吾郎kimura560

UPDATE

UPDATEは、テーブル内の既存データを変更するためのクエリです。具体例を見てみましょう:

UPDATE users SET username = 'root' WHERE username = 'admin';

最初のUPDATE usersが「usersテーブル内のデータを更新したい」ことをDBMSに伝えています。その後に続くSET username = 'root' WHERE username = 'admin'では、WHERE句で指定している条件(username = 'admin')に合致するレコードのusernamerootという文字列に変更するよう指示しています。このクエリが実行されるとレコードが更新されます:

idusernamepassword
1田中太郎pass123
2山田次郎456word
3鈴木三郎secret789
4rootp4ssw0rd
5木村吾郎kimura560

DELETE

DELETEは、テーブル内の既存データを削除するクエリです。WHERE句を使って削除するレコードの条件を指定します:

DELETE FROM users WHERE username = '田中太郎';

最初のDELETE FROM usersで「usersテーブル内のデータを削除する」ことをDBMSに伝え、WHERE句を使って「username田中太郎と一致する」という削除するデータの条件を指定しています。このクエリが実行されると条件に合致したレコードが削除されます:

idusernamepassword
2山田次郎456word
3鈴木三郎secret789
4rootp4ssw0rd
5木村吾郎kimura560

DELETEを使う場合は注意が必要です。もし条件を一つも指定せずにDELETEクエリを実行するとテーブル内の全てのレコードが削除されてしまいます。例えば下記のようにWHERE句のないDELETEクエリを実行すると、その下の表のように登録されていたデータが全て消えてしまいます:

DELETE FROM users;
idusernamepassword

SQLクエリにおけるシングルクォーテーションの役割

先程のSQLクエリの解説で度々登場したシングルクォーテーション'の役割について触れたいと思います。SQLクエリにおいてシングルクォーテーションで囲まれた部分は、文字列型(テキスト型)のデータであることを示します。具体例を見てみましょう:

DELETE FROM users WHERE username = '田中太郎';

このクエリを受け取ったDBMSは、シングルクォーテーションで囲まれている田中太郎が文字列型のデータであると解釈します。もしシングルクォーテーションで囲まれていなかった場合、DBMSは田中太郎というデータをどのように解釈すべきか理解できず、誤った操作を行ったりエラーを引き起こしたりする可能性があります。SQLインジェクション攻撃においてもシングルクォーテーションは重要な役割を果たすため、シングルクォーテーションの使い方を正しく理解し、適切に扱うことが重要です。

SQLインジェクションについて知っておきたいこと

データベースやSQLについて理解したところで、さっそく本題であるSQLインジェクションについて見ていきましょう。SQLインジェクションは深刻なセキュリティリスクであり、影響度や被害の大きさから特に対策を急がなければならない脆弱性と言われています。その概要は以下の通りです:

脆弱性が発生する可能性のある場所SQLクエリを扱っている部分
想定される被害情報漏洩、データ改ざん、認証回避、プログラム不正実行など
影響度
対策方法プレースホルダ、エスケープ処理

SQLインジェクションとは

SQLインジェクション(SQL injection:SQLi)とは、SQLクエリの扱い方に不備がある場合に発生する脆弱性の一種です。悪意のある攻撃者は、SQLインジェクションを利用して不正なSQLクエリを入力し、データベースを不正に操作します。例えば、一般的なWebサービスにみられるログイン機能にSQLインジェクションの脆弱性があった場合、攻撃者はユーザー名やパスワードの入力欄にDBMSを混乱させるような不正なSQLクエリを入力・送信し、ログイン認証を回避できます。その結果、以下のような被害が発生する可能性があります:

  • データベース内の情報が盗まれ、二次攻撃に悪用される
  • データベース内の機密情報が閲覧・改ざんされる
  • データベースサーバーを経由して別のサーバーに対して不正なプログラムを実行する

SQLインジェクション攻撃の仕組み

そもそもログイン認証とは、ログインフォームに入力されたユーザー名とパスワードの組み合わせがデータベースに登録された情報と完全に一致する場合のみマイページへのログインを許可する機能です。ここでDBMSがこの条件判定をどのように行っているのか見ていきたいと思います。まずは、正規のユーザーがユーザー名木村吾郎とパスワードkimura560を入力してログインボタンを押した場合のSQLクエリを見てみましょう:

SELECT * FROM users WHERE username = '木村吾郎' AND password = 'kimura560';

このクエリを受け取ったDBMSは、顧客情報が管理されているusersテーブルからユーザー名が木村吾郎と一致し、かつパスワードがkimura560と一致するレコードを探します。該当するレコードが存在した場合はそのアカウント情報が登録されていることを返答し、存在しなかった場合はアカウントが存在しないと返答します。

もしこのログイン機能にSQLインジェクションの脆弱性があった場合、攻撃者はどのように不正にデータベースを操作するのでしょうか。例えば、攻撃者がユーザー名の入力欄に適当なユーザー名' OR 1 = 1; --と入力したとします。そしてパスワードの入力欄には任意の文字列(例えば11zz33)と入力し、ログインボタンを押します。この場合、次のようなクエリがDBMSに送信されます:

SELECT * FROM users WHERE username = '適当なユーザー名' OR 1 = 1; --' AND password = '11zz33';

このクエリにおいて注目すべきは、ハイフン-という記号が連続している--という部分です。SQLクエリにおいて--とは「--以降のクエリは全て無視してよい」という意味を持ちます(コメントアウトという)。つまり、DBMSは上記のクエリを以下のように解釈します:

SELECT * FROM users WHERE username = '適当なユーザー名' OR 1 = 1; --

ここで重要なのは、パスワードに関する条件が消えている点です。さらに1 = 1という常に成立する条件によってユーザー名による条件判定も無効化されています。このような不正な入力によって攻撃者は、どのような任意のユーザー名とパスワードを入力してもログインできます。つまり、SQLインジェクションの脆弱性が存在するログイン機能は、誰でもログインできる機能と化してしまうのです。

警告:SQLインジェクション攻撃を試みることは絶対にしないでください(法律により罰せられます)。
この記事はSQLインジェクションに対する正しい対策を理解するためのものであり、攻撃行為を推奨するものでは決してありません。

SQLインジェクションの被害事例

これまでにSQLインジェクションによって多くの被害が発生してきました。以下では、最近発生した被害事例を紹介します:

  • マリンネット株式会社(2024年)
    • システム監視チームがサイトへの異常なアクセスを確認したため調査したところSQLインジェクション攻撃が発生していると判明
    • 登録されていたメールアドレスの一部が流出した可能性がある
    • 公式情報:https://www.marine-net.com/static/ja/MarineNet_8APR24.pdf
  • 統計数理研究所(2023年)
    • データベースサーバに対してSQLインジェクション攻撃があったと判明
    • 研究参加者のメールアドレス5,527件が漏洩した可能性がある
    • 公式情報:https://www.ism.ac.jp/kouhou/news/20230412.html
  • 株式会社フルノシステムズ(2022年)
    • サポートセンターのサーバーに対して海外からSQLインジェクション攻撃を受けたと判明
    • 製品サポートサイトのお問い合わせフォームに入力されていた個人情報1,068件が流出した
    • 公式情報:https://www.furunosystems.co.jp/news/info/20230127001469.html
  • 株式会社矢野経済研究所(2022年)
    • WebサイトのサーバにSQLインジェクションによる不正アクセスがあったことを確認
    • 会員のメールアドレスと暗号化されたログインパスワードが最大101,988件漏えいした可能性がある
    • 公式情報:https://www.yano.co.jp/announce/781
  • 兵庫県西宮市役所(2021年)
  • ナカバヤシ株式会社(2020年)
    • サイトのデータ処理に不審な点があり調査したところ、SQLインジェクション攻撃を受けていたと判明
    • 顧客情報最大12万件が流出した可能性がある
    • 公式情報:https://www.nakabayashi.co.jp/news/2020/info/699

具体的なSQLインジェクション対策

SQLインジェクションの基本的な対策方法は、「プレースホルダ」を使用することと「エスケープ処理」を実装することが挙げられます。それぞれ具体例とともにわかりやすく解説していきます。

プレースホルダとは

プレースホルダ(Prepared Statement:プリペアドステートメントともいう)とは、ユーザーの入力値のように動的に値が変わる(変数という)部分をあらかじめ明示して、SQLクエリを作成する仕組みです。プレースホルダ以外の部分は、変数の影響を受けないため安全にクエリを扱うことができます。具体的なプレースホルダの使用方法はDBMSの種類によって異なりますが、ここではMySQLの例を見てみましょう:

SELECT * FROM users WHERE username = :username AND password = :password;

上記において:username:passwordがプレースホルダです。ここにユーザーが入力した値が代入され、クエリが実行されます。このプレースホルダ以外の部分は、ユーザーがどのような値を入力しても変わらないため、SQLインジェクションを防ぐことができます。

エスケープ処理とは

エスケープ処理とは、シングルクォーテーション'やセミコロン;など、SQLクエリにおいて特別な意味を持つ記号を単なる普通の文字として扱うようにする処理のことです。具体的には、ユーザーから入力された値にこれらの特殊文字が含まれているかどうかを確認し、もし含まれている場合はそれを単なる文字列に変換する処理を行います。もし攻撃者が不正なSQLクエリを入力したとしてもエスケープ処理によって無害な文字列として変換されるため、SQLインジェクションを防ぐことができます。以下はPHPを利用してMySQLに接続し、エスケープ処理を行うためのプログラムの例です:

// データベース接続情報
$servername = "サーバー名";
$username = "ユーザー名";
$password = "パスワード";
$dbname = "データベース名";

// データベースに接続
$conn = new mysqli($servername, $username, $password, $dbname);

// 接続エラーをチェック
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

// ユーザーからの入力データ
$userInput = "It's a pen."; // ここではシングルクォーテーションを含む文字列を例としている

// エスケープ処理を行う
$escapedInput = $conn->real_escape_string($userInput);

// エスケープ処理された入力を使用してクエリを実行する
$sql = "INSERT INTO users (username) VALUES ('$escapedInput')";

if ($conn->query($sql) === TRUE) {
    echo "New record created successfully";
} else {
    echo "Error: " . $sql . "<br>" . $conn->error;
}

// データベース接続を閉じる
$conn->close();

PHP・MySQLにおけるSQLインジェクション対策例

PHPとMySQLを使っている場合のSQLインジェクション対策についてもう少し詳しく見てみましょう。PHPとMySQLには、PDO(PHP Data Objects)や、MySQLi(MySQL Improved Extension)といった安全にデータベースにアクセスするための手法があります。ここでは簡単なPDOの使用例を見てみましょう:

// データベース接続情報
$host = 'ホスト名';
$dbname = 'データベース名';
$username = 'ユーザー名';
$password = 'パスワード';

try {
    // PDOオブジェクトの作成
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);

    // エラーモードを例外に設定
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    echo "データベースに接続しました。";
} catch (PDOException $e) {
    echo "データベース接続エラー: " . $e->getMessage();
}

上記の例では、エラーモードを例外に設定し、SQLの処理に関してエラーが発生した際にエラーメッセージを表示しています。さらにPDO::setAttributeメソッドでプリペアドステートメントを強制するように設定することで、SQLインジェクション対策を行うことができます:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

ただし、PHPのバージョン5.3.5までのPDOには、データベースの接続時に文字コードを指定する方法が用意されていないため注意が必要です。対処方法としては、以下のようにMySQLの設定ファイルを指定します:

$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password, array(
    PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/etc/mysql/my.cnf', // 設定ファイルを指定
    PDO::MYSQL_ATTR_REAT_DEFAULT_GROUP => 'client',
    PDO::ATTR_EMULATE_PREPARES => false,
));

そして、指定した設定ファイル/etc/mysql/my.cnfに以下を追記します:

[client]
default-character-set=utf8

また、エスケープ処理を実装する方法としては、PHPのmysqli_real_escape_stringメソッドを使うことも有効です:

$username = mysqli_real_escape_string($conn, $unsafe_username);
$sql = "SELECT * FROM users WHERE username = '$username'";

Java・MySQLにおけるSQLインジェクション対策例

先程はPHPとMySQLを使用している場合のSQLインジェクション対策の例をみましたが、ここではJavaとMySQLにおける対策方法を見ていきたいと思います。Javaの場合は、PreparedStatementクラスを使用することでプリペアドステートメントを作成し、SQLクエリ内のパラメータを安全に処理できます:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DatabaseManager {

    private static final String DB_URL = "jdbc:mysql://ホスト名:ポート/データベース名";
    private static final String USER = "ユーザー名";
    private static final String PASS = "パスワード";

    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS)) {
            String username = "ユーザーが入力したユーザー名";
            String password = "ユーザーが入力したパスワード";

            // ここではSELECT文を例としている(他のSQLクエリも同様に対策可能)
            String sql = "SELECT * FROM users WHERE username = ? AND password = ?";

            // プリペアドステートメントを使用してSQLクエリを実行する
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, username);
                pstmt.setString(2, password);
                pstmt.executeQuery();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

また、エスケープ処理による対策方法も見てみましょう:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class DatabaseManager {

    private static final String DB_URL = "jdbc:mysql://ホスト名:ポート/データベース名";
    private static final String USER = "ユーザー名";
    private static final String PASS = "パスワード";

    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
             Statement stmt = conn.createStatement()) {

            String username = "ユーザーが入力したユーザー名";
            String password = "ユーザーが入力したパスワード";

            // エスケープ処理を行って安全なSQLクエリを生成する
            String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
            ResultSet rs = stmt.executeQuery(sql);
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

また、ユーザーの入力値に対するバリデーションやフィルタリングを行うことも効果的です。ユーザーからの入力がデータベースに送信される前に、入力の形式や範囲を適切に制限します。例えば、数値を入力するべき欄には、数値以外の入力を受け付けず、エラーメッセージを表示する機能を実装します。詳細は、OWASP(Open Web Application Security Project)という世界的なオープンソースプロジェクトが提供している SQL Injection Prevention Cheat Sheet にまとめられていますのでぜひご活用ください。

脆弱性診断が必要な理由

これまで見てきたように、SQLインジェクションは重大なセキュリティリスクです。新しいシステムを開発する際には、先に述べた対策をしっかりと行うことでSQLインジェクション対策が可能です。しかしながら、既存のシステムや提供中のWebサービスにおいては、まずはSQLインジェクションの脆弱性があるかどうかを確認することが対策への第一歩です。

いまなら無料脆弱性診断実施中!

テストオウルでは、期間限定で無料の脆弱性診断サービスを提供しています。当社の脆弱性診断サービスは、国際認定資格を保有するセキュリティのプロフェッショナルがお客様のシステムやWebサービスに潜む脆弱性を専門的に診断し、セキュリティ対策を支援いたします。セキュリティは現代のビジネスにとって極めて重要な要素です。情報漏洩や不正アクセスなどのリスクを最小限に抑えるために、ぜひ無料脆弱性診断サービスをご活用ください。詳細やお申し込みに関しては、こちらのフォームよりお気軽にお問い合わせくださいませ。

まとめ

SQLインジェクションに関してデータベースやSQLの基本から理解し、PHPやJavaにおけるプレースホルダやエスケープ処理の具体的な対策例までご紹介しました。SQLインジェクションの脆弱性は重大なセキュリティリスクです。定期的に脆弱性診断や対策の確認を行い、企業の大切な資産を守りましょう。

参考文献

よかったらシェアしてね!
目次