My Photo

« February 2014 | Main | April 2014 »

March 28, 2014

PHPメモ037:cURLでHTTPSなサイトにアクセスする

PHPで外部のWebサービスを利用するため、POSTメソッドのHTTPリクエストを送信する処理について調べた。単純にWebコンテンツを持ってくるだけなら file_get_contents() でもいいのだが、今回はWEBサービスを利用するためのリクエストで、レスポンスのステータスコードによって処理を適宜分ける必要があるかもしれないので file_get_contents() は使わないことにした。
#file_get_contents() でもステータスコードを取ることはできるようだ。(記事末の参考サイトを参照)

file_get_contents() 以外の方法を探して下記のサイトを見つけた。
#"php post curl"でググるとこれ以外にもたくさん参考になるサイトが出てくる。

ウェブル@soraiyさんのPHP で凄 く簡単に GET/POST 送信ができる関数を勝手に改良した | zaru blog

cURLが使える環境だったので「cURL版」をベースに、GETは必要ないので不要な部分を削った関数を作成。こんな感じ。

function requestByPost($url, $params = array(), $headers = array()) {
	
	$ch = curl_init($url);
	
	curl_setopt($ch, CURLOPT_POST, TRUE);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
	curl_setopt($ch, CURLOPT_TIMEOUT, 0);	// いつまでも待つ
	$res_data = curl_exec($ch);
	
	$res_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
	
	if (preg_match('/^20.$/', $res_code) !== 1) {	// ステータスコードが200番台以外の場合
		$res_data = NULL;
	}
	
	if (curl_errno($ch) != 0) {
		log_info('curl error:[' . curl_errno($ch) .'] '. curl_error($ch));
	}
	
	curl_close($ch);
	
	return $res_data;
}

これを利用して下記のようなコードによりHTTPSでBASIC認証ありのリクエストを送ってみた。接続先は他社Webサービスのテスト環境。URLは適当に書き換えてある。

function requestForService($id, $password, $name) {
	
	$url = 'https://api.test.service.jp/create';
//	$url = 'https://api_test.service.jp/create';	// 追記。これならエラーにならない。下の追記を参照。
	$basic_auth_id   = 'hogehoge';
	$basic_auth_pass = 'hoge1234';
	
	// POSTパラメータ
	$req_params = array(
		'id'       => $id,
		'password' => $password,
		'name'     => $name
	);
	
	// HTTPリクエストヘッダ
	$str_basic_auth = $basic_auth_id . ':' . $basic_auth_pass;
	$req_headers = array('Authorization: Basic ' . base64_encode($str_basic_auth));
	
	// リクエストを送信してID発行を要求
	$res_data = requestByPost($url, $req_params, $req_headers);
	
	return $res_data;
}

しかし、うまくいかなかった。ステータスコードを調べると "0"。
"0" というのはありえないので、HTTPで通信する以前でエラーになっているようだ。
curl_errno()、curl_error() によると

[51] SSL: certificate subject name '*.service.jp' does not match target
 host name 'api.test.service.jp'

サーバーの証明書のエラー?しかし "*.service.jp" ならば "api.test.service.jp" とマッチするのでは?よくわからない。
とりあえず

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);

を追加してサーバー証明書の検証をしないようにしたところ、レスポンスが帰ってきてステータスコードは200番台、想定どおりの動きになった。
しかし本番では証明書の検証はしないというのは、まずいような。

エラーの原因はサーバ側かクライアント側か。
自分が作っている方がこちらがHTTPクライアントである。ということWEBブラウザと同様にクライアント証明書が必要じゃないか、という気がして調べた。
cURLのサイトに証明書が置いてあるので、それを設定するということが書いてあったので同じようにやってみた。証明書を持ってきてサーバ上に置き、下記のコードを追加。

curl_setopt($curl, CURLOPT_CAINFO, '/var/www/ca/cacert.pem');

しかし、これでも同じエラーが出た。
うーん。原因はこちら側(クライアント側)ではなくサーバー側の証明書なんだろうか?

2014/03/28 22:30追記
エラーが解消した。Webサービスの運営に質問したら「ホスト名を "api_test.service.jp" でもアクセスできます。そっちならエラーにならないかも。」と回答があったので、そのように変更したところ、エラーが発生しなくなった。上記のような curl_setopt() による設定による証明書関連の設定していない。つまりクライアント証明書を設定しておらず、サーバー証明書の検証はしているはず。
エラーメッセージにある "*.service.jp" は "api.test.service.jp" とはマッチしないが、"api_test.service.jp" とはマッチするということだろうか。
しかし、ホスト名にアンダースコアは使えないはずだが。まあ試験環境だし、そもそも他社のサービスだし、深く考えないでおこおう^^;

あと、こちらのサーバーに "/etc/pki/CA/cacert.pem" があった。cURLではデフォルトでこの証明書をクライアント証明書として使っているのだろうか?
問題は解決したが、またわからないことが出てきた。

参考サイト:
ウェブル@soraiyさんのPHP で凄く簡単に GET/POST 送信ができる関数を勝手に改良した | zaru blog

PHPのcURL関数について:
PHP: 基本的な curl の使用法 - Manual
PHP: cURL 関数 - Manual

cURLのサイト:
curl and libcurl
cURL - Extract CA Certs from Mozilla:証明書が置いてある

cURLについて参考にしたサイト:
cURL | PHP プログラミング解説
メモ: [PHP][cURL] cURLでSSL(https)のCA証明書警告の回避や設定
【php】curlでSSL通信をCA証明書のチェックありで at softelメモ
PHP - ローカルサーバからWebサービスAPIへSSL接続時に出るcurlのエラー対処 - Qiita
サーバのSSL CA(認証局)証明書が古くてcurl がエラーになる件 - うまい棒blog
cURL - Manual (マニュアル日本語訳)

証明書について参考にしたサイト:
SSL通信時発生する証明書エラーとその仕組みを理解する - Qiita
OpenSSL証明書操作/VeriSign - # cat /var/log/stereocat | tail -n3
デジタル証明書の形式
証明書のファイル形式について

その他参考にしたサイト:
PHP の file_get_contents でステータスコードを取得する

March 27, 2014

ファイルダウンロードによるXSS その3

関連記事:
ファイルダウンロードによるXSS その1
ファイルダウンロードによるXSS その2

対策

アップロード時とダウンロード時に以下の対策を実施する。

アップロード時の対策
  • 拡張子が許可されたものかをチェックする。
  • 画像の場合はマジックバイトを確認する。

拡張子のチェックについてはアップロードファイルによるサーバ側スクリプト実行で説明した。
マジックバイトの確認にはPHPの場合 getimagesize() という関数が利用できる。

array getimagesize (string $filename [, array &$imageinfo ])
返り値は最大 7 つの要素からなる配列。
0番目および1番目の要素は、それぞれ画像の幅と高さ
2番目の要素は IMAGETYPE_XXX 定数のひとつで、画像の形式を表す。
3番目の要素はIMGタグで直接利用できる文字列 height="yyy" width="xxx"。

この関数は引数として画像のファイル名を受け取り、画像の縦横サイズと画像の形式などを配列として返す。
画像の形式の値の一部を下表に示す。

getimagesize()が返す画像形式の情報(一部)
定数
1 IAMGETYPE_GIF
2 IMAGETYPE_JPEG
3 IMAGETYPE_PNG

アップロードファイルによるサーバ側スクリプト実行で改良したアップロードスクリプトをさらに getimagesize() を使用して改良し、XSS脆弱性に対処する。対処後のスクリプトを upload3.php とする。
画像ファイルのチェック関数 check_image_type() を以下に示す。

// function check_image_type($imgfile, $tofile)
//   $imgfile : チェック対象画像ファイル名
//   $tofile : ファイル名(拡張子チェック用)
function check_image_type($imgfile, $tofile) {
  // 拡張子の取得とチェック
  $info = pathinfo($tofile);
  $ext = strtolower($info['extension']);
  if ($ext != 'png' && $ext != 'jpg' && $ext != 'gif') {
    die('拡張子はpng、gif、jpgのいずれかを指定ください');
  }
  // 画像タイプ取得
  $imginfo = getimagesize($imgfile);
  $type = $imginfo[2];
  // 以下、正常な組み合わせの場合はreturnしていく
  if ($ext == 'gif' && $type == IMAGETYPE_GIF)
    return;
  if ($ext == 'jpg' && $type == IMAGETYPE_JPEG)
    return;
  if ($ext == 'png' && $type == IMAGETYPE_PNG)
    return;
  // 最後までreturnしない組み合わせはエラー
  die('拡張子とイメージ形式が一致しません');
}

upload3.php でこの check_image_type() を呼び出している部分を以下に示す。

$tmpfile = $_FILES['imgfile']['tmp_name'];
$orgfile = $_FILES['imgfile']['name'];
if (! is_uploaded_file($tmpfile)) {
  die('ファイルがアップロードされていません');
}
// 画像のチェック
check_image_type($tmpfile, $orgfile);
$tofile = get_upload_file_name($orgfile);
ファイルダウンロード時の対策
  • Content-Typeを正しく設定する。
  • 画像の場合は、マジックバイトを確認する。
  • 必要に応じてContent-Dispositionヘッダを設定する。

・Content-Typeを正しく設定する
PDFファイルダウンロードによるXSS脆弱性のサンプルは、Content-Typeの間違いが原因だった。Content-Typeを正しく指定すれば脆弱性はなくなる。Content-Typeを正しく指定することはIEに限らずすべてのブラウザで必要である。
ダウンロードスクリプト経由ではなくファイルを公開領域に保存する場合は、Webサーバの設定の確認をする。Aapacheでは mime.types という設定ファイルにContent-Typeの設定が保存されている。あまり使われていないソフトウェアを利用する場合や mime.types を自分で設定した場合は、ブラウザ側で認識できるContent-Typeであることをチェックするすべきである。

・画像の場合はマジックバイトを確認する
ダウンロードスクリプトを用いてファイルをダウンロードする際には、ダウンロード時にもマジックバイトを確認するようにすれば、なんらかの原因で不正なファイルがWebサーバに紛れ込んでいても確実な対策が可能となる。
以下にアップロード時の対策で用いて check_image_type() を利用するダウンロードスクリプトの改良版を示す。

$mimes = array('jpg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif');

$file = $_GET['file'];
$info = pathinfo($file);       // ファイル情報の取得
$ext = strtolower($info['extension']);     // 拡張子
$content_type = $mimes[$ext]; // Content-Typeの取得
if (!$content_type) {
  die('拡張子はpng、gif、jpgのいずれかを指定ください');
}
$path = UPLOADPATH . '/' . basename($file);
check_image_type($path, $path);
header('Content-Type: ' . $content_type);
readfile($path);

・必要に応じてContent-Dispositionを設定する
ダウンロードしたファイルをアプリケーションで開くのではなくダウンロードできさえすればよい場合は、レスポンスヘッダに「Content-Disposition: attachment」を指定する方法がある。この場合は Content-Type も「application/octet-stream」にするとファイルタイプ上も「ダウンロードすべきファイル」とう意味になる。

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="sample.pdf"

・その他の対策
ここまでの対策は脆弱性を防止するための必要最小限のチェックである。例えば、マジックバイトのチェックのみでは利用者のブラウザで放蕩に表示できるかまでは確認できない。
このため、Webアプリケーションの仕様策定時に以下のようなチェックを行うかどうか検討するとよい。

  • ファイルサイズ以外の縦横サイズ、色数などのチェック
  • 画像として読み込めるかどうかのチェック
  • ウイルス・スキャン
  • コンテンツの内容チェック(自動まはた手動
    ・アダルトコンテンツ
    ・著作権を侵害するコンテンツ
    ・法令、公序良俗に反するコンテンツ
    など

参考:利用者のPCに対象アプリケーションがインストールされていない場合

Content-Typeに該当するアプリケーションが利用者のPCにインストールされていない場合、その Content-Type はブラウザにとって未知のものとなり、XSSの可能性がある。この問題の対応は容易ではない。確実な対処として以下の方法がある。

  • コンテンツを配信するサーバのドメインを別にする。
  • Content-Dispositionヘッダを付ける。

上記の方法には副作用があるため、確実性は劣るものの副作用のない対処としては以下の方法があります。

  • アプリケーションが想定したURLかどうかをチェックする。
  • コンテンツの閲覧に必要なアプリケーションの導入を利用者に注意喚起する。

参考文献:体系的に学ぶWebアプリケーションの作り方 4.12.3 ファイルダウンロードによるクロスサイト・スクリプティング

体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践
徳丸 浩

ソフトバンククリエイティブ 2011-03-03
売り上げランキング : 4070

Amazonで詳しく見る
by G-Tools

参考サイト:
PHP: getimagesize - Manual

March 26, 2014

事故現場に遭遇

20140325_00

昨日、いつものように会社に向かって自転車をこいでいたら、途中の山手通りでトレーラーが横転していた。
写真の左の横断歩道、毎日行き帰りで渡っている。もしかしたら自分が巻き込まれていたかもしれない。こわっ!!
朝でボケーっとしていたが一気に目が覚めた。
電柱が2本折れ、電線が地面に垂れ下がっていたので、自転車を抱え、電線をまたいで会社に向かった^^;

昼にYahoo!JAPANを見たらトップに出ていた。
帰りに通ったときは片側交互通行にして復旧工事をしていて、今朝には新たな信号機が設置されていた。

事故が起こった場所は青信号でスタートした車が加速していく所なのだが、急カーブで下り坂。仕事帰りにカーブの外側(写真の左奥、青い服の作業員?3人がいる辺り)で信号待ちをしていると
「車が突っ込んできそうで怖いなあ」
といつも思っていたのだが、まさに危惧していたとおりの事故だ。

この場所の周り住宅もオフィスビルもある。事故が通勤時間帯の8時台に起きたことを考えると、複数の死者がでてもおかしくなかった。重傷者がおらず、歩行者が巻き込まれなかったのは運が良かったとしかい言いようがない。


この事故についてニュース記事:トレーラー横転、信号機なぎ倒す 品川の山手通り:朝日新聞デジタル

March 23, 2014

ファイルダウンロードによるXSS その2

関連記事:ファイルダウンロードによるXSS その1

脆弱性の原因

この脆弱性の原因にはIneternet Explorer 特有の仕様が影響している。IEはファイルタイプの判定にHTTPレスポンスのContent-Typeヘッダ以外に、URL上の拡張子やファイルの中身を利用している。
その判定仕様は非公開だが、以下のような挙動が判明している。

コンテンツが画像の場合

Content-Typeの他に、画像ファイルのマジックバイトがファイルタイプの判定に利用される。
マジックバイトとはファイルタイプ識別のためにファイルの先頭に置かれた固定の文字列である。JPEG、PNGのマジックバイトを下表に示す。

画像形式 マジックバイト
JPEG \xFF\xD8\xFF
PNG \x89PNG\x0D\x0A\x1A\x0A

IE7以前のデフォルト設定では以下のようにファイルタイプを判定する。

・Content-Typeとマジックバイトが一致する場合
Content-Typeが示すファイルタイプを採用する。

・Content-Typeとマジックバイトが一致しない場合
どちらも無視し、ファイルの内容からファイルタイプを推測する。ファイルの内容にHTMLタグが含まれていればHTMLと判定する場合もある。

コンテンツが画像以外の場合

IEのバージョンを問わず次のような仕様であると推測されている。
IEが扱うことのできるContent-Typeの場合、Content-Typeに従って処理をする。扱えるContent-Typeはレジストリの HKEY_CLASSES_ROOT\MIME\Database\Content Type に登録されている。
IEが扱うことのできないContent-Typeの場合、URLに含まれる拡張子からファイルタイプを判定する。このルールは複雑であるので説明しない。
前の記事で攻撃用URLの作成のためにPATHINFOとして「/a.html」を加えたのは、このURL中の拡張子からファイルタイプを判定する仕様を悪用するためである。

参考文献:体系的に学ぶWebアプリケーションの作り方 4.12.3 ファイルダウンロードによるクロスサイト・スクリプティング

体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践
徳丸 浩

ソフトバンククリエイティブ 2011-03-03
売り上げランキング : 4070

Amazonで詳しく見る
by G-Tools

« February 2014 | Main | April 2014 »

May 2017
Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ

日本blog村

  • にほんブログ村 IT技術ブログへ
  • にほんブログ村 アニメブログへ
  • にほんブログ村 サッカーブログ アルビレックス新潟へ

好きな音楽家

メモ

XI-Prof