ウェブアプリのセキュリティをちゃんと知ろうPHP でやるお (^ω^)
基本的な考え方脆弱性とは何か対策ではなく、原理を知るバグは必ずある入力、処理、出力の仕様を明確化する
脆弱性って何?脆弱性とは「バグ」です。正確には、「第三者」が「ウェブサイト利用者」や「ウェブサイト運営者」に対して悪用することが可能になる「バグ」です本来、「セキュリティ対策」という特別な作業がある訳ではない普通に「バグ」のないプログラムを書くことが、「セキュリティ対策」
対策ではなく、原理を知ろう「バグ」を直すには、「バグ」が起こる原理を知らなければならない「バグ」を直す魔法などない
バグは必ずあるとはいえ、バグを作らない人はいません過去、現在、未来、僕たちの作ったプログラムには必ずバグがあるもちろん、バグを作らない努力は最大限行うべき気が付いたり、指摘されたら、すぐに直すことこそが一番重要
入力、処理、出力の仕様を明確化するバグとは仕様を守らないことウェブアプリは非常に複雑仕様も複雑、バグも作りやすい細かい単位(入力、処理、出力)で、仕様を明確にし、バグの出現箇所から原因をすぐに特定できるようにする
ウェブアプリの入力、処理、出力入出力ウェブサーバウェブアプリ(PHP など)外部 API サーバ(Facebook API 、決済会社など)入出力処理入出力データベースサーバ(MySQL など)ウェブブラウザ
ウェブアプリの入力、処理、出力ちゃんと仕様を答えられるようにしよう入出力の仕様ウェブサーバーを通したウェブブラウザとの入出力の仕様データベースサーバーとの入出力の仕様外部 API サーバーとの入出力の仕様その他さまざまな機器や、サーバーとの入出力の仕様処理の仕様ウェブアプリの処理の仕様ライブラリやフレームワークが行う処理の仕様
今日は以下の仕様について考えてみようウェブサーバーを通したウェブブラウザからの入力の仕様ウェブサーバーを通したウェブブラウザへの出力の仕様データベースサーバーへの出力の仕様どのようなリクエストを処理すべきか?という仕様を考えよう
ウェブサーバーを通したウェブブラウザからの入力の仕様を考えようPHP に入ってくる値は何かを知る可変長のバイト列 (文字列ではない!!)GET パラメータPOST パラメータアップロードファイルリクエストヘッダ (Cookie など)実際の処理に渡すべき値は何かを考える文字列か、バイト列か?文字コードは何か?(ウェブサーバーでバイト列を処理することってあまりないので、 PHP では基本的に文字コードのバリデーションは必要だと思って良い)長さはどうか?どういう文法や構造を持つデータ?入力された値を実際の処理に渡すべき値かどうかを確認することを「バリデーション」という
GET パラメータのバリデーション# PHP に入ってくる可能性があるのは可変長のバイト列$url = $_GET['url'];if (!mb_check_encoding($url, 'UTF-8')) throw new Exception('文字列ではない');# この時点で $urlは UTF-8 でエンコーディングされた文字列ということが保証される$url_length = mb_strlen($url, 'UTF-8');if ($url_length > 512) throw new Exception('文字列が長すぎる');# この時点で $urlは UTF-8 でエンコーディングされた 512 文字以下の文字列ということが保証されるif (!preg_match('/\As?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:@&=+$,%#]+\z/u', $url)) throw new Exception('URL として不正');# この時点で $urlは Http URL であることが保証される$url_info = parse_url($url);if ($url_info['host'] !== 'ohma-inc.com') throw Exception('外部サイトの URL');# この時点で $urlは ohma-inc.com の Http URL であることが保証される
アップロードファイルのバリデーションif (!$_FILES['file']) throw new Exception('ファイルがアップロードされなかった');$file = $_FILE['file'];# この時点でmulipart/form-data によって file というパラメタ名で# ファイルが送信されたことが保証されるif (!is_uploaded_file($file['tmp_name']) or $file['error'] !== 0)  throw new Exception('アップロードエラー');$filename = $file['tmp_name'];# この時点で $filename は HTTP_POST によって送信されたファイルを# 一時保存しているファイルのパスであり、 php.ini に設定された# アップロードファイルのファイルサイズ以内であることが保証される$info = getimagesize($filename);if (!$info or !isset($info['mime']) or $info['mime'] === 'image/gif') throw new Exception('アップロードされたファイルが GIF じゃない');# この時点で $filename は HTTP_POST によって送信されたファイルを# 一時保存しているファイルのパスであり、 php.ini に設定された# アップロードファイルのファイルサイズ以内であり# GIF のマジックバイトを持つことが保証される
ウェブサーバーを通したウェブブラウザへの出力の仕様を考えようブラウザへ渡すべき値は何かを考える文字列なのか、バイト列なのか?文字コードは何?(動的に画像を生成するような場合以外は、だいたい文字列を出力することが多いよね)出力するデータの、文法やデータ構造は? (MIME タイプは何?)ブラウザが正しく文法やデータ構造、文字コードを理解し処理できるには何が必要?X-Content-Type-Options: nosniff を送ったうえで、Content-Type は正しく遅れているか文法やデータ構造を守った文字列やバイト列を生成するにはどうしたらいいか文法やデータ構造を正しく出力するための手法シリアライズ、エスケープテンプレートに埋め込む場合に重要なことは、文法をまたがらず、たった一つのリテラルのみを作ることXSS は、正しく HTML や JavaScript を生成出来ていない場合や、ブラウザに正しく Content-type や文字コードを伝えられていない場合などに発生する
phpapache の設定HTML に正しくコンテンツを認識させるheader('Content-Type: text/html; charset=utf-8');header('X-Content-Type-Options: nosniff');header('Content-Type: application/json; charset=utf-8');header('X-Content-Type-Options: nosniff');AddDefaultCharset utf-8Header set X-Content-Type-Options nosniff
正しいデータを生成する1ダメな例例えば $data = "\"><script>alert(1)</script><a href=\"";...<a href="/search?q=<?= $data ?>"></a>...
正しいデータを生成する1$dataJS文字列URL ComponentCSS識別子JS  識別子CDATAPCDATAPCDATAPCDATARCDATAhtmlspecialchars(rawurlencode($data), ENT_QUOTES, 'UTF-8')
正しいデータを生成する1正しい例...<a href="/search?q=<?= htmlspecialchars(rawurlencode($data), ENT_QUOTES, 'UTF-8') ?>"></a>...
ダメな例正しいデータを生成する2...<script>var data = "<?= $data ?>";...
正しいデータを生成する2$dataJS文字列URL ComponentCSS識別子JS  識別子CDATAPCDATAPCDATAPCDATARCDATApreg_replace('/<\//u', '\\u003c\\u002f', json_encode($data))
正しい例正しいデータを生成する2...<script>var data = <?= preg_replace('/<\//u', '\\u003c\\u002f', json_encode($data)); ?>;if (typeof(data) !== 'string') throw Error('文字列じゃない!');...
ダメな例正しいデータを生成する3...<a onclick="var data = '<?= $data ?>'; ......
正しいデータを生成する3$dataJS文字列CSS識別子JS  識別子CDATAPCDATAPCDATAPCDATARCDATAhtmlspecialchars(json_encode($data), ENT_QUOTES, 'UTF-8');
正しい例正しいデータを生成する3...<a onclick="var data = <?= htmlspecialchars(json_encode($data), ENT_QUOTES, 'UTF-8'); ?>; if (typeof(data) !== 'string') throw Error('文字列じゃない!');......
正しいデータを生成する(まとめ)正しいデータを生成するって大変だよね関数名や、関数パラメータも長いよね間違いの元だよねなので、フレームワークやライブラリを積極的に活用しよう
データベースサーバーへの出力の仕様を考えようデータベースへ渡すべき値SQLSQL を正しく生成するには?プリペアードステートメントを使う別の言い方すると、値の埋め込みにはプレースホルダを使う正しい SQL を生成できない = SQL インジェクションが発生する
ダメな例 (正しくない SQL が生成される可能性がある)正しい例 ( "?" がプレースホルダ)正しい SQL を生成する$db->execute('SELECT name FROM member WHERE member_id = "' . $member_id . '"');$stmt= $db->prepare('SELECT name FROM member WHERE member_id = ?');$stmt->bind_param($member_id);$stmt->execute();
どのようなリクエストを処理すべきか?という仕様を考えようリクエストされる状況にはどんなものがあるかを考えるscript の src属性に埋め込まれるimgの src属性に埋め込まれるXMLHttpRequestによる呼び出し意図しないクリック意図したクリックリクエストに対して、処理をすべきかを考える自サイトからリクエストされたかGET か POST かXMLHttpRequestからリクエストされたかCSRF はここの仕様バグによっておこる
自サイトからリクエストされたことを保証するリクエスト元で予測不可能なトークンを cookie や session に埋め込む同じトークンをフォームに埋め込み POST するリクエスト先でcookie や session からトークンを読み込んで、 POST されたトークンと同じ値かどうかを確認する
リクエスト元リクエスト先自サイトからリクエストされたことを保証する$token = base64_encode(openssl_random_pseudo_bytes(64));setcookie('csrf_token', $token);...<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">...if ($_POST['csrf_token'] !== $_COOKIE['csrf_token'])throw new Exception('想定外');
毎回これを書くのも大変なので、フレームワークやライブラリを活用しましょう。自サイトからリクエストされたことを保証する
正しくサイトを作るって難しいよね

ウェブアプリのセキュリティをちゃんと知ろう (毎週のハンズオン勉強会の資料)