SQLインジェクションとその対策(PHP + PDO)

> この記事は、[North Detail Advent Calendar 2019](https://qiita.com/advent-calendar/2019/northdetail) の21日目の記事です。

## はじめに

先日、社内で[「安全なウェブサイトの作り方」](https://www.ipa.go.jp/security/vuln/websecurity.html)を使った読書会が行われました :green_book:

SQLインジェクションについて議題に上がったので、
勉強がてら実際に試してみよう！というのが今回の趣旨になります。

## SQLインジェクション？

> SQLインジェクション（英: SQL Injection）とは、
> アプリケーションのセキュリティ上の不備を意図的に利用し、
> アプリケーションが想定しないSQL文を実行させることにより、
> データベースシステムを不正に操作する攻撃方法のこと。
> 
> https://ja.wikipedia.org/wiki/SQLインジェクション

「Injection」という単語が「注入する」という意味のようです(知らなかった)。

アプリケーションが実行するSQL文に**変なもの**を注入して
意図しない動作をさせる攻撃なので「SQL Injection」ってことですね :smiley:

## 実際に起こしてみる

### データを作る

まずはMySQLにテスト用データを作成します。

```SQL
CREATE TABLE users (
  id int NOT NULL AUTO_INCREMENT,
  name varchar(10),
  PRIMARY KEY (id)
);

INSERT INTO
  users(name)
VALUES
  ('フグ田サザエ'),
  ('フグ田マスオ'),
  ('磯野波平'),
  ('磯野フネ'),
  ('磯野カツオ'),
  ('磯野ワカメ'),
  ('フグ田タラオ'),
  ('タマ')
;
```
:arrow_down:
![スクリーンショット 2019-12-19 9.53.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/6af2251d-7050-d8a1-1d39-8d0ce784ed8b.png)

:tangerine:
:cat:
:tangerine:

### テスト用画面を作る

次は、IDを入力してユーザーを検索できる画面を作ります。

PDOクラスを使ってMySQLにアクセスしています。

```PHP:injection.php
<?php
  try {
    $records = [];
    $id = $_POST['id'];

    $pdo = new PDO(
      'DSN',
      'USERNAME',
      'PASSWORD'
    );
    $prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
    $prepare->execute();
    $records = $prepare->fetchAll(PDO::FETCH_ASSOC);
  } catch(PDOException $e) {
    echo $e->getMessage();
  }
?>
```


![スクリーンショット 2019-12-19 10.19.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/33c78625-633f-b0fa-4581-032f0361760f.png)
:arrow_down: 
![スクリーンショット 2019-12-19 10.20.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/78385dcc-5b75-f711-8b32-d55b4929c6cd.png)



うーんシンプル。

ここで…文字列結合を使ってクエリ組み立てていますね :imp:

```PHP
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();
```

### 攻撃してみる

先ほどの例では「3」と検索して「磯野波平」のデータを取得しました。

じゃあ次は「0 OR TRUE; --」で…

![スクリーンショット 2019-12-19 10.39.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/595b7f5d-ab3e-5ff1-22a4-bacf42198729.png)
:arrow_down: 
![スクリーンショット 2019-12-19 10.21.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/a5e8c5cc-61e2-ec0b-4b2e-e6336509e602.png)

`users` テーブルの全レコードがヒットしてしまいました :scream:

ユーザー入力をそのまま採用した結果、こんなSQL文になりました。
WHERE句が常にTRUEになるので、全てのレコードが対象になったんですね。

```SQL
SELECT * FROM users WHERE id = 0 OR TRUE; --;
```


ちょっと付け加えて、お次は「0 OR TRUE; DELETE FROM users; --」で検索…

![スクリーンショット 2019-12-19 10.40.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/b2e83b81-931d-311b-7732-062620bc9c64.png)
:arrow_down: 
![スクリーンショット 2019-12-19 10.24.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/1cc4f075-8ecc-600c-c93c-cab45b51b89a.png)

あれ？さっきと同じ？？

MySQLを覗いてみると…

![スクリーンショット 2019-12-19 10.24.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/ca7a412b-566f-4511-da80-6630677e1d7d.png)

な　ん　と　い　う　こ　と　で　し　ょ　う
匠の手によって全てのレコードが削除されてしまいました :skull:

先ほどの例とだいたい同じですが、
検索クエリを正常終了させた後にDELETE文が実行されちゃってますね…


```SQL
SELECT * FROM users WHERE id = 0 OR TRUE; DELETE FROM users; --;
```

## 対策する

> SQL 文の組み立ては全てプレースホルダで実装する。

「安全なウェブサイトの作り方」では[mysqli](https://www.php.net/manual/ja/class.mysqli.php)が使われていましたが、PDOでもプレースホルダを使って実装できます！

先ほどは文字列結合を使ってクエリを組み立てていましたが、
今度は「:id」のように`コロン+パラメータ名` の形でクエリ内に記載します。

そしてクエリ実行の前に `bindValue` 関数を使ってバインドします。
(`bindParam` 関数でもバインドできます。 変数が評価されるタイミングが異なるそうです。)


```PHP:修正前
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();
```
:arrow_down: 

```PHP:修正後
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = :id;');
$prepare->bindValue(':id', $id, PDO::PARAM_INT);
$prepare->execute();
```

プレースホルダを使ってバインドすると、クエリとして解釈されないようです。
あくまで「値」として扱われるため、変なものを注入した場合でもクエリとして実行されないんですね。

実際にさっきの攻撃を試してみましょう

![スクリーンショット 2019-12-19 10.49.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/1799fd40-6109-b2b4-b021-a2dfdf221bda.png)
:arrow_down: 
![スクリーンショット 2019-12-19 10.48.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/add89183-22d4-f4d6-673d-0dffab4b654e.png)

何も検索されなくなりました！

![スクリーンショット 2019-12-19 10.49.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/7ffba072-0727-867c-4abf-671e157620ef.png)

今度は磯野家も無事でした… :family_mwgb:

## まとめ

SQLインジェクションによる攻撃と、その対策方法について実際に試してみました。

SQLインジェクションやプレースホルダによる対策自体は知っていて、学生時代にJavaで一度やったことある程度でした。
その時は理解が浅い状態だったので、仕組みも含めて復習できて良かったです :relaxed: :two_hearts:

## 明日は

「North Detail Advent Calendar」 22日目は @yamatohkd さんです！ :clap:
→ [スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった](https://qiita.com/yamatohkd/items/0f5b9b031ec0976d872a)

## 参考

* 安全なウェブサイトの作り方
https://www.ipa.go.jp/security/vuln/websecurity.html

* PDOクラス
https://www.php.net/manual/ja/class.pdo.php

* PDOStatementクラス
https://www.php.net/manual/ja/class.pdostatement.php

* サザエさんの登場人物
https://ja.wikipedia.org/wiki/サザエさんの登場人物