Webサイトの代表的な脆弱性対策

Webサイトのプログラムにバグや設計ミスがある場合, それらは脆弱性と呼ばれ, 攻撃者の的にされてしまいます.
攻撃された場合, システムの破壊, 情報漏えい, マルウェア感染などの被害の恐れが生まれます.

代表的な攻撃手法

脆弱性をついた攻撃のうち, 有名なものをまとめました.

SQLインジェクション (SQL Injection)

ユーザーからの入力を受け付けるようなWebサイトに見られる脆弱性をついた攻撃手法です.
ユーザーログインを行うWebサイトでは, 入力されたユーザー名とパスワードを, サーバーが所持しているデータベースに送りつけます.
本来は自分のユーザー名とパスワードを入力するべきですが, 攻撃者が悪意のある文字列を入力フォームに打ち込むと, 正しいパスワードを知らなくても第三者のフリをしてログインできてしまいます.

クロスサイトスクリプティング (XSS: Cross Site Scripting)

SQLインジェクションと同様に, ユーザーからの入力を受け付けるようなWebサイトに対する攻撃です.
入力フォームに悪意のあるコードを打ち込むことで, 一般ユーザーが意図しない誤操作 (クッキー情報の漏えい等) を行ってしまいます.
ちなみに, Cross Site Scriptingを略するとCSSとなる気がするのですが, Webページを作るためのCSSという言語と名前がかぶってしまうため, XSSと略されます.

バッファオーバーフロー

大容量のデータをデータを送りつけることで, システムが誤作動を起こす場合があります.
ユーザーからのデータは, サーバー上のバッファと呼ばれる記憶領域に一度保管されます.
このデータサイズがバッファサイズを超えてしまった場合, リターンアドレスと呼ばれる部分を書き換え, 悪意のあるコードを無理やり実行させることができてしまいます.

Cross Site Request Forgery (CSRF)

Webサイトにアクセスした一般ユーザーは, 自分が意図しない危険な操作を強要されてしまいます.
Webサイト上に配置された悪意のあるスクリプトを実行してしまう, もしくは別に存在する危険なWebサイトへと転送 (リダイレクト) することで引っかかってしまいます.
攻撃者が直々に攻撃するわけではない上, 一般ユーザーは脆弱性を持つWebサイトを不用意にクリックしないことでしか防げない, 非常にタチの悪い攻撃手法です.

攻撃への対策

代表的な攻撃手法を列挙しましたが, ここではSQLインジェクションへの対策をご紹介します.
入力フォームは, ユーザー名とパスワードの2つとします.
また, データベースはユーザー名を保持する「name」, パスワードを保持する「password」の2つのカラムを持っています.

SQLインジェクションへの対策

PHPで書かれた簡単なログインフォームを例に考えます.
下記のコードを見てみましょう (脆弱性を持つ悪い例です).

<?php header("X-XSS-Protection: 0");?>
<?php
    $logged_in = false;

    // ログインボタンが押された場合
    if (isset($_POST["login"])) {
        $username = $_POST["username"]; // ① ユーザー名を$usernameに格納
        $password = $_POST["password"]; // ② パスワードを$passwordに格納
        try {
            $dbh = new PDO('mysql:dbname=form;host=localhost', 'root', '');
        } catch (PDOException $e) {
            exit('データベースに接続できませんでした'.$e->getMessage());
        }
        $sql = "SELECT * FROM user_info WHERE name='$username' AND password='$password'"; // ③ データベースへの命令を$sqlに格納
        $stmt = $dbh->prepare($sql); // ④ データベースへ$sqlを送付
        $stmt->execute();
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!empty($data)) {
            $logged_in = true;
            echo "ログイン成功";
        } else {
            $logged_in = false;
            echo "ログイン失敗";
        }
    }
?>

①, ②でユーザー名, パスワードをそれぞれ$username, $passwordという変数に格納します. その後③でデータベースへの命令文を作っていますが, これをよく見ると「データベース上のnameカラムが$usernameと一致し, かつpasswrodカラムが$passwordと一致すればログインを許可しますよ〜」と書いてます.
ここで, $usernameを「aaa」 (なんでもいいです), $passwordを「’ OR ‘A’ = ‘A」としてみましょう.

$sql = "SELECT * FROM user_info WHERE name='aaa' AND password='' OR 'A' = 'A'";

よく見ると「〜 OR ‘A’=’A’」という形になっています.
‘A’=’A’は必ず真となるため, データベース上のnameカラム, passwordカラムと入力が一致しようがしまいがログインできてしまいます.

ここで, 以下のようにコードを書き換えてみましょう.

<?php header("X-XSS-Protection: 0");?>
<?php
    $logged_in = false;

    // ログインボタンが押された場合
    if (isset($_POST["login"])) {
        $username = $_POST["username"];
        $password = $_POST["password"];
        try {
            $dbh = new PDO('mysql:dbname=form;host=localhost', 'root', '');
        } catch (PDOException $e) {
            exit('データベースに接続できませんでした'.$e->getMessage());
        }
        //$sql = "SELECT * FROM user_info WHERE name='$username' AND password='$password'"; // 変更前
        //$stmt = $dbh->prepare($sql); // 変更前
        //$stmt->execute(); // 変更前
        $stmt = $dbh->prepare('SELECT * FROM user_info WHERE name= ? AND password= ?'); // ④ データベースへの命令を$sqlに格納 (③の代わり)
        $stmt->execute(array($username, $password)); // ⑤ ④に入力する変数の指定
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!empty($data)) {
            $logged_in = true;
            echo "ログイン成功";
        } else {
            $logged_in = false;
            echo "ログイン失敗";
        }
    }
?>

プレースホルダと呼ばれる書き方を使っています.
③と④を比較するとわかりますが, 本来$username, $passwordと書きたい部分を「?」で代用しています.
その後, ⑤にて「?」に何を格納するのかを指定します.
これで, SQLインジェクションに対抗できるはずです.