初心者に学ぶ 脆弱なWebアプリケーションの作り方〜入社時研修で作ったWebアプリの脆弱性についてのコードレビュー３選

# はじめに

実務未経験でWebエンジニアとして入社してからの2カ月で受けた社内研修についての振り返りです。

今回は「Webアプリケーション基礎」の課題について、 **セキュリティ** に関するレビュー内容に焦点を当てて、整理します（レビューで色々と指摘をいただいた、かつ、重要と感じたため）。

結論としては、 **SQLインジェクション対策** や、 **クロスサイトスクリプティング（XSS）対策** 、 **バリデーション** がうまくできていなかった、という初歩的な内容です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527226/5ba786e9-4294-7e3f-5a45-7a0a89345010.png)

https://wasbook.org/

タイトルは、こちらの徳丸本を意識しています。（最近読了しましたが、今回の記事では直接的に触れていません）

## 記事の目的

同時期に実務未経験でWebエンジニアになって交流している方々（他社の）が、

**「他の人が初期にどのような注意を受けているか聞いてみたい」**

と言っていたため、私は自分が書いたコードをレビューしてもらった機会はまだ研修のみですが、どのような点をレビューで教えてもらったかについて、共有したいと思います。

振り返りを通して、私自身現在忘れてしまわずに身になっているか確認していきます。

## 想定する読者

想定する読者は、実務未経験の初学者の方です。（過去の自分を想定して書いてます）

## どんな社内研修？

社内研修は、Web開発全般についての基礎的な研修で、以下のカリキュラムでした。

1. PHPの基礎（PHP、オブジェクト指向）
1. データベースの基礎（SQL、MySQL）
1. Webアプリケーション基礎（HTML、CSS、CRUD、Cookie、セッション）

これらの内容についての課題を解いていき、 GitLab でマージリクエストして、社内のエンジニアの方にレビューをいただく、という流れで実施しました。

これまでに PHP と SQL については、コードレビューを題材にして記事を書きました。

https://qiita.com/ryamate/items/d87e7168111fc1ef4935


https://qiita.com/ryamate/items/c42ade6b8f3f65358ef9

<br>

Webアプリケーション基礎の課題は例えば下記のようなものでした。

**「HTML, CSS, PHPを利用して、以下の画像と似たような画面を実装してください」**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527226/23f18685-9943-1ef4-e608-bc69a9cf2f11.png)

※ このスクショは当時の自分の回答

<br>

このような問題が数ステップあり、OKもらえるまで修正しました。
今回の内容は私がもらったリアルなレビューなので、初心者が見落としがちなポイントなのかなと思っています。
※ 記事にする都合上、レビューの文面は意味が変わらない程度に編集しています。

## 目次

1. [SQLインジェクション対策](#1-sqlインジェクション対策)
1. [クロスサイトスクリプティング（XSS）対策](#2-クロスサイトスクリプティングxss対策)
1. [バリデーション](#3-バリデーション)

# 1. SQLインジェクション対策

まずは、Webアプリケーションのセキュリティに関して、SQLインジェクション対策ができていなかったことに対するレビューです。

## 1.1. 「ユーザーの入力値をそのままSQLに入れないでください」

```php:レビュー前
/**
 * リクエストされた検索条件に従ってproductテーブルからデータを取得する
 *  - 商品名は部分一致での検索とする。
 *  - 価格は入力値以上の範囲検索とする。
 *  - 表示件数の選択肢は10件,20件,30件とする。
 *  - 作成日時の降順、IDの昇順で取得・表示とする。
 *
 * @param array $searchValues (バリデーションエラーがなかった)検索条件
 * @return array $products リクエストされた検索条件で、productsテーブルからデータ取得された商品
 */
function searchProducts(array $searchValues):array
{

    // データベースに接続する
    $link = dbConnect();

		// productsテーブルからリクエストされた検索条件に従って商品名、色、商品単価を取得するSQL文
		$sql = <<<SQL
		SELECT
		    product_name,
		    color,
		    price
		FROM
		    products
		WHERE
		    product_name LIKE '%{$searchValues['productName']}%'
```

### コードレビューの内容

:::note
**入力値をそのままSQLに入れることは絶対にしてはいけません。**

**SQLインジェクション** によって不正にデータベースを操作されてしまいます。(例：商品名の入力を`'; {任意のSQL文}`とすると、任意のSQL文が実行できてしまいます)

対策として **プリペアドステートメント** と**バインド変数**を用いる方法があり、PDOでも利用可能です。

:::

<br>

```php:レビュー後
function searchProducts(array $searchValues):array
{

    // データベースに接続する
    $link = dbConnect();

    // productsテーブルからリクエストされた検索条件に従って商品名、色、商品単価を取得するSQL文
    $sql = <<<SQL
    SELECT
        product_name,
        color,
        price
    FROM
        products
    WHERE
        product_name LIKE :productName
        AND
        price >= :price
    ORDER BY
        created_at DESC,
        product_id ASC
    LIMIT
        :limit
    ;
    SQL;

    $stmt = $link->prepare($sql);

    // SQLインジェクションの対策として、プリペアドステートメントとバインド変数を用いる
    // - LIKE検索のワイルドカードについて、エスケープする
    $stmt->bindValue(':productName', '%'.addcslashes($searchValues['productName'], '\_%').'%', PDO::PARAM_STR);
    $stmt->bindValue(':price', $searchValues['price'], PDO::PARAM_INT);
    $stmt->bindValue(':limit', $searchValues['limit'], PDO::PARAM_INT);

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

    // データを取得する
    $products = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // データベースの接続を解除する
    dbDisconnect($link);

    return $products;
}
```

絶対にしてはいけないことをしてしまいました。
独学の頃にも学んだことはありましたが、重要性を理解できていませんでした！…

# 2. クロスサイトスクリプティング（XSS）対策

続いて、Webアプリケーションのセキュリティに関して、クロスサイトスクリプティング（XSS）ができていなかったことに対するレビューです。

## 2.1. 「ユーザーの入力値を表示する際のエスケープを必ず行ってください」

```php:レビュー前
<main>
    <div class="mx-4 my-2">
        <h1>Cookie</h1>
        <p><?php echo $name ?>さん、ようこそ！</p>
```

### コードレビューの内容

:::note
**ユーザーの入力値を表示する際のエスケープは、必ず行ってください。**
:::

※ レビュー後に調べたこと

- **HTML** や **JavaScript** を注入・変形される **XSS（Cross-Site Scripting）脆弱性** が生じるため、 **エスケープ** する必要がある
- **エスケープ** とは、メタ文字の持つHTMLの文法上特別な意味を打ち消すこと

<br>

```php:レビュー後
<main>
    <div class="mx-4 my-2">
        <h1>Cookie</h1>
        <p><?php echo escape($name) ?>さん、ようこそ！</p>
```

```php
<?php

/**
 * XSS(クロスサイト・スクリプティング)対策として、特殊文字をエスケープする
 */
function escape($string)
{
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
```

XSS 対策は課題の中で部分的に対策はしていたのですが、対策の抜け漏れがあり、自身のセキュリティへの認識の甘さを感じました。

研修でのレビューを通して、これらのセキュリティはWebアプリケーションを作成する上で当然わかっていないといけない、そしてそれを理解できていない、ということが浮き彫りになりました。

転職のためのポートフォリオとして作成したWebアプリケーションに、ザルすぎるセキュリティホールがなかったか今更ながら不安になりました…（今後、ソースコードを振り返る機会を作りたいです）

# 3. バリデーション

続いて、 **バリデーション** について、適切に入力値の検証ができていないことに対するレビューです。
**バリデーション（入力値検証）** は、ユーザーが入力した値が不正な値ではないかチェックすることです。

## 3.1. 「『パラメータが入力されているか』のみの検証は`empty()` ではなく、 `isset()` を使ってください」

```php:レビュー前
<?= $_GET['productName'] === '0' || !empty($_GET['productName']) ? escape($_GET['productName']) : '';?>
```

### コードレビューの内容

:::note
今回は「パラメータが入力されているか」を検証する必要があります。動作としては問題ないと思いますが、そもそも`empty()`を使って判定する必要はありません。

[PHPマニュアル](https://www.php.net/manual/ja/function.empty.php#:~:text=empty()%20%E3%81%AF%E6%9C%AC%E8%B3%AA%E7%9A%84%E3%81%AB%20!isset(%24var)%20%7C%7C%20%24var%20%3D%3D%20false%20%E3%81%A8%E5%90%8C%E3%81%98%E3%81%93%E3%81%A8%E3%82%92%E7%B0%A1%E6%BD%94%E3%81%AB%E8%A8%98%E8%BF%B0%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%A0%E3%81%91%E3%81%A7%E3%81%99%E3%80%82) には以下の通り記載があります。

> `empty()` は本質的に `!isset($var) || $var == false` と同じことを簡潔に記述しているだけです。


「パラメータが入力されているか」（`!isset($var)`）の判定は必要 ですが、「入力されたパラメータの値が何か」（`$var == false`）の判定は不要 です。

なので、今回のケースでは`isset()`を用いるのが適切です（空入力を判定したい場合は、`$var === ''`の判定を加える）。

:::

※ レビュー後に調べたこと（`isset()`が何を返すかを `var_dump` で確認）

```php
// アクセスした(GETパラメータがない)とき
var_dump($_GET['price']); // null
var_dump(isset($_GET['price'])); // boolean false

// '0'を入力して検索ボタンを押したとき
var_dump($_GET['price']); // string '0' (length=1)
var_dump(isset($_GET['price'])); // boolean true

// 未入力で検索ボタンを押したとき
var_dump($_GET['price']); // string '' (length=0)
var_dump(isset($_GET['price'])); // boolean true
```

<br>

```php:レビュー後
<?= isset($_GET['productName']) ? escape($_GET['productName']) : '';?>
```

<br>

## 3.2. 「バリデーションでの整数の判定方法は正規表現を使ってください」

```php:レビュー前
// 価格が10000000000以上、もしくは整数ではない場合、エラーメッセージ「価格は9999999999以下の整数で入力してください。」を表示する
if ((!ctype_digit($searchValues['price']) && $searchValues['price'] !== '')) {
    $errors['price'] = '価格は正の整数で入力してください。';
} elseif ((int)$searchValues['price'] >= 10000000000) {
		$errors['price'] = '価格は9999999999以下で入力してください。';
}
```

https://www.kabanoki.net/3204/

このページを参考に考えた結果、なぞなぞ不正解でした…

<br>

### コードレビューの内容

:::note
バリデーションでの整数の判定方法は、 正規表現 を使ってください。

```php
preg_match('/^-?[0-9]+$/', $a)
```
:::

<br>

```php:レビュー後
    // 価格が10000000000以上、もしくは整数ではない場合、エラーメッセージ「価格は9999999999以下の整数で入力してください。」を代入する
    if (((int)$searchValues['price'] >= 10000000000) || !preg_match('/^-?[0-9]+$/', $searchValues['price'])) {
        $errors['price'] = '価格は9999999999以下の整数で入力してください。';
    }
```

正規表現、奥が深そう…。

# おわりに

## 「セキュリティについて体系的に勉強しておいてください」

研修を終えて、「セキュリティは重要なので、現状のような指摘されてから対処するという場あたり的な状態から抜け出すために体系的に勉強しておいてほしい。まずはこれを読んでおいてほしい」と言われたものが以下の参考資料です。

https://www.ipa.go.jp/security/vuln/websecurity.html

通読したのですが、代表的な脆弱性と、その脅威、対応策がまとめられていました。代表的な攻撃手段を一通り把握できていると、ここにはセキュリティの対策が必要ではないかと開発時に思い当たるようになると思うので、とても勉強になりました。（巻末にチェックシートがついてるので、対策に漏れがないかのチェックにも使えそうです）

また、「はじめに」でも表紙を貼りましたが、徳丸浩先生の著書『安全なWebアプリケーションの作り方』（通称、徳丸本）についても、最近通読しました！が、まだ理解が浅いです。（688ページありました！…）

https://wasbook.org/

<br><br>

ちなみに、徳丸本読んだ後はこれがおすすめ！という取り組みがもしあれば教えていただけますと幸いです。

一つ教えていただいた TryHackMe というのが今は気になっています。

https://tryhackme.com/

<br><br>

今回の記事は以上です！ありがとうございました。