ことれいのもり

初心者でも分かるSQLインジェクションの仕組みと対策方法

はじめに

自作CMSを作る過程で、SQLインジェクションへの対策としてバインドパラメーターを用いました。

SQLインジェクションとは何か、どのような対策を施したかをまとめておきます。

前提

言語

  • PHP 8
  • MySQL

参考リンク

SQLインジェクションとは?

悪意のある入力をSQL文として認識させ、データベースのテーブルを削除したり情報流出をさせる攻撃方法のことです。

今回は、ユーザーネームとパスワードが必要なログイン画面を例として説明します。

攻撃方法

攻撃する画面の説明

ログイン画面の画像

このようなユーザー名とパスワードを入力するログイン画面があったとします。

このとき、以下のようなSQL文が実行されているとします。

SELECT * FROM users WHERE username = '$username' AND password = '$password';

例えば、 username = ‘admin’ password = ‘password111’ のときに実行されるSQL文は

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

これがデータベースに存在していればログインが成功し、存在していなければログイン失敗になります。

攻撃してみる

この設定下で、ユーザーがこのように入力したとします。

SQLインジェクション攻撃をするログイン画面の画像

' OR '1'='1

このときに実行されるSQL文は

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '';

ここが少し難しいところです。

SQLインジェクション攻撃をするときに入力した文字の図解

元のSQL文は、ユーザーが入力された文字列に ‘ ‘ をつけてSQL文の値として実行します。 今回であれば、「' OR '1'='1」に ' ' をつけるので、結果として「' ' OR '1'='1’」というusernameが入力された扱いになります。 パスワードは空なので ' ' になります。 この説明は上の画像を見てもらうと分かりやすいと思います。

攻撃したときの値の評価

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '';

このSQL文の評価を見てみましょう。 SQL文では、ANDはORよりも優先度が高くなります。 つまり、

SELECT * FROM users WHERE (username = '') OR ('1'='1' AND password = '');

かっこをつけるとこのような見た目になります。

間違ったSQL文の評価画像

username = ' ' この部分は空のユーザー名が存在しないので FALSE になります。 '1' = '1' これは等しいので TRUE になります。 password = ' ' これはパスワードが間違っているので FALSE になります。 後半部分は TRUE AND FALSE なので、結果は FALSE 全て合わせると FALSE OR FALSE になるのでログインできないですね!

・・・ 違います!! これは間違っています!!

よく思い出してください、ORを入れたのはユーザー名の入力欄ですよね? ということは、ORの影響範囲はユーザー名の条件だけです。 そしてANDは、ユーザー名とパスワードにかかっていたので、ユーザー名にのみ影響します。 つまり、

正しいSQL文の評価画像

username = ' ' この部分は空のユーザー名が存在しないので FALSE になります。 '1' = '1' これは等しいので TRUE になります。 この2つにのみORは影響するので、FLASE OR TRUE で TRUEになります。 password = ' ' これはユーザー名にのみ影響を受けて評価されますが、ユーザー名は今回 FALSE なので無視されます。 結果として全体で TRUE になり、ログインできてしまうというわけです。

SQLインジェクションの対策方法

今回のように不正ログインされてしまう理由は、ユーザーの入力した値をそのままSQL文に組み込んでいることが原因です。 その対策として、バインドパラメータがあります。 バインドパラメーターはユーザーの入力をSQL文として実行するのではなく、分離したデータとして扱う方法です。 この分離したデータのことをプレースホルダーと言います。

// :username とすると、ユーザー名として入力した値をここに埋め込む変数といった意味になる
// :password も同様にパスワードとして入力した値をここに埋め込む変数といった意味になる
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");

// 埋め込み方を指定する
// :usernameには変数$usernameを紐付け、string型の文字列だと教える
$stmt->bindValue(':username', $username, PDO::PARAM_STR);

// :passwordには変数$passwordを紐付け、string型の文字列だと教える
$stmt->bindValue(':password', $password, PDO::PARAM_STR);

// 実行する
$stmt->execute();

プレースホルダーはbindValueを使って、紐付ける変数名と型を指定します。

このような方法で記述すると、SQLインジェクション攻撃を防ぐことができます!

SQL文を実際に埋め込むことは止めましょう。

実際の使用例

実際に使っているログイン画面の処理部分はセキュリティの観点から載せませんが、バインドパラメーターを用いている部分を紹介します。

記事編集画面でサムネイル画像を新しい物に更新する部分です。

SQL文の中身は全てプレースホルダーを使い、bindValueを使って紐付けています。

// サムネイルパスを更新
public function updateThumbnailPath($articleId, $newFileName, $newFilePath)
{
  try {
    $stmt = $this->db->prepare("UPDATE " . $this->config->get('tables')['thumbnails'] . " SET file_name = :file_name, file_path = :file_path WHERE article_id = :article_id");
    $stmt->bindValue(":file_name", $newFileName, PDO::PARAM_STR);
    $stmt->bindValue(":file_path", $newFilePath, PDO::PARAM_STR);
    $stmt->bindValue(":article_id", $articleId, PDO::PARAM_INT);

    return $stmt->execute();
  } catch (PDOException $e) {
    echo "サムネイルパスの更新に失敗しました: " . $e->getMessage();
    return false;
  }
}

おわりに

SQLインジェクションの説明とその対策方法についてまとめました。

攻撃者がユーザー名に SQL文を入力した場合の評価部分が難しかったと思います。

この攻撃によって本来見ることができないデータを盗み見られたり流出する恐れがあります。

しっかり理解しておきましょう!