SQLインジェクションとは

## はじめに
Webセキュリティについて学習をするため、OWASP TOP10 2017でアプリケーションリスク 第１位となったインジェクションの一つSQLインジェクションについて改めて概要、脅威、原因、対策についてまとめます。

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

SQLインジェクションとはアプリケーションの不備をついて、想定していないSQL文が実行されることにより、データベースを不正に操作される攻撃手法です。

情報漏えい、データ改ざん、サービスの継続不可など影響度の大きさから危険度が特に高く、緊急で対応が必要な脆弱性です。

## SQLインジェクションの脅威を知る

### 環境
以下の環境で確認しました

- PHP 7.3.8
- MySQL  5.7.22

### ログイン機能のサンプル
以下は簡単なユーザーログイン機能のサンプルです。データベースの users テーブルでログインIDとパスワードを管理。PHPのログインフォームから送信された該当のログインIDとパスワードが usersテーブル内にあれば、正しいログイン情報としてログイン処理を進めます。

```sql
CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `login_id` tinytext,
  `password` tinytext,
  `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

```sql
mysql> select * from users;
+----+----------+----------+---------------------+---------------------+
| id | login_id | password | created             | updated             |
+----+----------+----------+---------------------+---------------------+
|  1 | taro     | password | 2019-08-13 13:43:31 | 2019-08-13 13:43:31 |
+----+----------+----------+---------------------+---------------------+
1 row in set (0.00 sec)
```

```php
<?php

// $dsn, $username, $passwordは宣言済みとする
$db = new PDO($dsn, $username, $password);
session_start();

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $login_id = $_POST['login_id'];
    $password = $_POST['password'];
    $sql = "SELECT * FROM users WHERE login_id = '{$login_id}' and password = '{$password}';";
    $prepare = $db->prepare($sql);
    $prepare->execute();
    $row = $prepare->fetch(PDO::FETCH_ASSOC);
    if ($row) {
      $_SESSION['login_id'] = $row['login_id'];
    }
}

?>
<html>
<body>

<?php if ($_SESSION['login_id']): ?>
<p>ログイン中: <?= $_SESSION['login_id']?></p>
<?php endif; ?>

<form method="post" action="login.php">
  <input type="text" name="login_id">
  <input type="password" name="password">
  <input type="submit" value="送信">
</form>
</body>
</html>
```

上記のコードをブラウザで表示すると以下のログインフォーム画面が表示されます。このログインフォームにログインID「taro」、パスワード「password」を入力するとログインに成功する簡単なサンプルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29755/386c4801-3bd3-4095-309d-c9433178f580.png)

しかし、サンプルコードにはSQLインジェクションの脆弱性があるため、プログラムの不備を突いた攻撃をすると不正ログインやデータの改ざんが行えます。

**注意事項**

* **絶対に本番サーバー、稼働サービスでは実行しないでください。必ず安全なローカル環境などでお試しください**
* **本検証はあくまでSQLインジェクションの脅威を知ってもらうための検証です**

`login_id` に以下の値を与えると

```sh
' or 1 = 1 ; --
```

以下のクエリーが生成されます。この場合、パスワード部はコメントアウトに無視され、かつ `1 = 1` により正となり不正ログインが成り立ちます。

```sql
SELECT * FROM users WHERE login_id = '' or 1 = 1 ; --' and password = '';
```

また次の事例として `login_id` に以下を与えると

```sh
’; delete from users; --
```

以下のクエリーが生成されます。今度は `delete from users;` が実行されてしまい、usersテーブルの中身が削除される結果となります。サービスの影響は計り知れません。


```sql
SELECT * FROM users WHERE login_id = ''; delete from users; --' and password = '';
```

## リテラル

まずSQLのリテラルについて知る必要があります。

```sql
SELECT * FROM users WHERE name = 'yamada';
```

SQLは以下の要素で構成されます。

キーワード(予約語)|SELECT FROM WHERE
---|---
演算子|=
識別子|name *
リテラル|'yamada'

SQL文中の `'yamada'` の部分をリテラルといい、文字列の場合は文字列リテラル、数字の場合は数値リテラルと呼びます。 `'yamada'` は文字列リテラルです。

## SQLインジェクションの原因

### 文字列リテラルのSQLインジェクション

ログイン機能サンプルは文字列リテラルが原因でした。

```sql
$sql = "SELECT * FROM users WHERE login_id = '{$login_id}' and password = '{$password}';";
```

とPOSTパラメーターをそのままSQL文と文字列結合をしているため、パラメーターとしてシングルコーテーション `'` 、SQLのコメントアウト `--` を利用することで、攻撃が有効になります。


### 数値リテラルのSQLインジェクション

数値リテラルの場合はシングルコーテーションを必要しませんので、 

```sql
SELECT * FROM users where age >= $age;
```

`$age` に `1; delete from users` の値が与えられた場合は、

```sql
SELECT * FROM users where age >= 1; delete from users;
```

のSQLが成立してしまい、ユーザー情報を全て削除されてしまいます。

## 対策
「[安全なSQLの呼び出し方](https://www.ipa.go.jp/files/000017320.pdf)」に安全なSQLの呼び出し要件は以下のように定義されています。

> ・ 文字列リテラルに対しては、エスケープすべき文字をエスケープすること
> ・ 数値リテラルに対しては、数値以外の文字を混入させないこと 

これらの解決のためプレースホルダーの利用が強く推奨されています。

```php
    $sql = "
    SELECT
        *
    FROM
        users
    WHERE
        login_id = ?
        and
        password =  ?
        ;
    ";
    $prepare = $db->prepare($sql);
    $prepare->execute([$login_id, $password]);
```

パラメーターの `?` をプレースホルダー、そこに実際の値を割り当てることをバインドと呼びます。プレースホルダーにバインドする方法は２種類あります。

* 静的プレースホルダー
* 動的プレースホルダー

### 静的プレースホルダー
プレースホルダーのままのSQL文と差し替え用の値両方をデータベースに送信し、データベース側でプレースホルダーへの割当を行うことを静的プレースホルダーといいます。セキュリティ上の観点では最も安全です。

### 動的プレースホルダー
アプリケーションのライブラリ側でプレースホルダーへの値の割当を行うのを動的プレースホルダーといいます。ただしライブラリ側で問題がある場合に脆弱性に発展してしまう可能性も否定できません。

特に理由がない限りでは静的プレースホルダーを利用するのが最適です。

## まとめ
SQLインジェクションの脅威を知ってもらい、その上で原因と対策方法についてまとめました。その影響度合いは甚大であることから、Webセキュリティの基礎として覚えていく必要があります。今後Webセキュリティについて学ぶ方の参考になれば幸いです。

## 参考
- 体系的に学ぶ安全なWebアプリケーションの作り方
- [安全なウェブサイトの作り方](https://www.ipa.go.jp/security/vuln/websecurity.html)
- [PHPでデータベースに接続するときのまとめ](https://qiita.com/mpyw/items/b00b72c5c95aac573b71)