ウウウウウウウウウウウウウ ウウウウウウウウ PHP ウウウウ ( ^ ω ^ )
ウェブアプリのセキュリティをちゃんと知ろう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 や文字コードを伝えられていない場合などに発生する
• php
• apache の設定
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
...
<a href="/search?q=<?= $data ?>"></a>
...
• ダメな例
• 例えば $data = "\"><script>alert(1)</script><a href=\"";
正しいデータを生成する1
CDATAPCDATA PCDATA PCDATARCDATA
JS文字列
JS 識別
子
URL
Component
CSS識別
子
$data
htmlspecialchars(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
CDATAPCDATA PCDATA PCDATARCDATA
JS文字列
JS 識別
子
URL
Component
CSS識別
子
$data
preg_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
CDATAPCDATA PCDATA PCDATARCDATA
JS 識別
子
JS文字
列
CSS識別
子
$data
htmlspecialchars(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(' 文字列じゃない! '); ...
...
正しいデータを生成する(まとめ)
• 正しいデータを生成するって大変だよね• 関数名や、関数パラメータも長いよね– 間違いの元だよね
• なので、フレームワークやライブラリを積極的に活用しよう
データベースサーバーへの出力の仕様を考えよう
• データベースへ渡すべき値– SQL
• SQL を正しく生成するには?– プリペアードステートメントを使う• 別の言い方すると、値の埋め込みにはプレースホ
ルダを使う• 正しい SQL を生成できない = SQL イン
ジェクションが発生する
• ダメな例 ( 正しくない SQL が生成される可能性がある )
• 正しい例 ( "?" がプレースホルダ )
正しい SQL を生成する
$stmt = $db->prepare('SELECT name FROM member WHERE member_id = ?');
$stmt->bind_param($member_id);$stmt->execute();
$db->execute('SELECT name FROM member WHERE member_id = "' . $member_id . '"');
どのようなリクエストを処理すべきか?という仕様を考えよう
• リクエストされる状況にはどんなものがあるかを考える– 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('想定外 ');
• 毎回これを書くのも大変なので、フレームワークやライブラリを活用しましょう。
自サイトからリクエストされたことを保証する
正しくサイトを作るって• 難しいよね