cakephperの日記(CakePHP, Laravel, PHP)


継続的WebセキュリティテストサービスVAddyを始めました!

2009-02-26

paginationにXSS攻撃の可能性があったのでチケット投げた

CakePHP 1.2で導入されたPagination機能はページング処理が簡単に導入できて最高ですが、今回paginator helperで、ある値がエスケープやurlencodeされていない箇所があり、クロスサイトスクリプティングXSS)の可能性があり、問題だと思ったので本家CakePHPTracバグチケットを投げてみました(今年の目標の一つにチケットを投げるというのがあったのでこれは達成です)

チケット投げて3日程度で、修正パッチが出ました。

https://trac.cakephp.org/changeset/8061

https://trac.cakephp.org/ticket/6134


日本コミュニティ内でのやりとりは下記にあります。

http://cakephp.jp/modules/newbb/viewtopic.php?viewmode=flat&topic_id=1779&forum=9


mark_storyさんがリリースしたパッチは、該当箇所の修正に加えて、helper.phpurl functionも修正していて、このurl functionを使ってURLを生成しているヘルパーの機能は、全てhtmlエスケープされるようになりました。

今まで、paginationを使ってクエリを引き回す記事は下記で書きましたが、追記でurlencodeしなきゃいけないということも書いてきました。

http://d.hatena.ne.jp/cakephper/20080907/1220796088

http://d.hatena.ne.jp/cakephper/20081118/1227005449

今回のパッチをあてると(もしくは将来パッチがあたったソースがリリースされると)、この問題も特に気にしなくてすみます。手動でリンクに値を動的に動的な値を埋め込む場合は、気にしなきゃいけませんけど。


paginationの値を引きついでいく記事でよく見かける(自分の過去の記事も含めて)、viewのファイルで下記のようにする

<?php $paginator->options = array('url' => $this->passedArgs ) ); ?>

という箇所ですが、このまま利用すると、passedArgsの値をrender前に、urlencodeとかエスケープしてない限りはXSSの問題が発生します。この問題も今回のパッチで解決できるので、もしそのまま本番環境で上記のようなコードを使っている人はパッチを当てておく(該当箇所を書き換えるだけ)ことをお勧めします。

2008-11-18

よくある検索画面でPaginationを使う

2008/12/25追記

ソースコードの中にコメントとプログラムを追記しました。これがないとXSSが発生する可能性があるのでご注意下さい。


cakePHP1.2 RC3を利用してます。

よくデータの管理画面なんかで、簡易検索機能として検索条件を入れて、その結果を一覧表示するのってありますよね。

Googleの検索画面なんかもそう。

この場合、検索条件を最初にPOSTして、その後、検索結果一覧画面が表示されます。結果一覧では検索条件を変えたりするために、検索入力エリアがあり、そこにPOSTした検索条件が保持されています。

これをCakeで作る場合、検索結果一覧はPagination機能を使ってお手軽にページングしたい、でも検索条件もフォームエリアに保持し続けたい、それらを1画面で表現したい、そんな時にどうするか。



まず、最初の検索条件をPOSTすると、$this->dataにデータが入ります。その後、Paginationでページングして2ページ目とかソートとかのリンクを遷移すると、POSTではなくURLの中にパラメータを入れるGETリクエストになり、データは$this->passedArgsに入ります。(POSTしたデータをページングで引き回す方法はここを参照: http://d.hatena.ne.jp/cakephper/20080907/1220796088 )

そこで、POST時は$this->dataから値を取得、Get時は$this->passwdArgsから値を取得します。Getの際に検索条件の指定も何も無い初回のGETなのか、PaginationのURLからの遷移なのかを判断します。これはcount($this->passedArgs)を使って配列の数を数えて1以上であればPaginationのURLからの遷移と判断します。

PaginationのURLからの遷移の場合は、フォームに検索条件の値を保持させるために、$this->dataに$this->passedArgsの値をセットします。


コントローラでは下記のようにします。

if( !empty($this->data) ){

	$search = $this->data[$this->name];

}elseif( count($this->passedArgs) ){

	$search = $this->passedArgs;

	$this->data[$this->name] = $search;

}

/* 2008/12/25追記
   $search配列は、不要なキーと値の組み合わせが入る可能性があるので
  必要なキーのみに絞り、値をurlencodeする処理が必要です。
  そうしないと、XSSが発生する可能性があります。
*/
   $search_list = array( 'key1', 'key2','key3' );

   $search_value = array();
   foreach( $search_list as $value ){
        $search_value[ $value ] = urlencode( $search[$value] );
    
   }



$this->set( 'searchword' , $search_value );

Viewでは下記のようにしてPaginationのURLを生成します。

<?php $paginator->options(array('url' =>  $searchword  )); ?>

2008-10-28

paginationのソート表示で、画像を使う

利用環境はCakePHP1.2RC3です

ViewのPaginationヘルパーで、下記のようにすると簡単にソート機能を使うことができます。

<?php echo $paginator->sort('id');?>

Paginationでの検索条件の引継ぎなどは、下記の記事を参照下さい。

http://d.hatena.ne.jp/cakephper/20080907/1220796088


今回、このソートの表示を文字列ではなく、画像にしたかったので下記のように対応。

<?php echo $paginator->sort( '<img src="hoge.jpg">', 'id', array('escape' => false));?>

昇順か、降順かによって画像を切り替えたかったため、下記のようにpassedArgsの値を見て表示する画像を変えた

    <?php if( $this->passedArgs['sort'] === 'id' && $this->passedArgs['direction'] === 'desc' ){
    		echo $paginator->sort( '<img src="hoge1.jpg">', 'id', array('escape' => false));
    	}else{
    	        echo $paginator->sort( '<img src="hoge2.jpg">', 'id', array('escape' => false));
    	}
    ?>

上記のようにすると、idの昇順時はhoge2.jpg、降順時はhoge1.jpgが表示されます。



Paginationの検索条件URLを取得する

利用環境はCakePHP1.2RC3です

下記の記事で書いた、

http://d.hatena.ne.jp/cakephper/20080907/1220796088

Paginationでの検索条件の引継ぎですが、基本的にsortやnextメソッド時に勝手にURLがくっついてくれるのですが、ソートとかページング以外の箇所のリンクにもその値を使いたいって時に。

<?php echo $paginator->url($paginator->options['url']); ?>

これでURLが出力されます。コントローラー名やアクション名のURLまで表示されるので、適宜加工するなりすれば別ページへの遷移に使えます。


追記(2008/11/5)

もっとお手軽に取得する方法がありました。

<?php echo $paginator->link( 'hoge title', array( 'controller' => 'aaa' ,'action' => 'hoge' ) , $paginator->options['url']); ?>

paginatorのlinkメソッドを使えばOK。第1引数はリンクのタイトル、第2引数はURLの構成(配列でcontrollerやaction名を指定できます)、第3引数がオプションになっていて、ここにパラメータ情報をセットしてあげればOK.

2008-09-07

フォームから送信された値を、Paginationで引き継ぐ方法

環境はcakePHP1.2RC2です。

検索画面などで、検索条件を入力して検索結果一覧を表示するようなものって結構ありますよね。そこで面倒なページング処理なんですが、CakePHP1.2から利用できるPaginationの機能を使えば簡単にページング実装できるよって話。ただ、普通にPaginationだけ使うと、例えば次へのリンクの中に検索条件のパラメータを入れてくれないので、ページングのリンクをクリックすると検索条件がすべて消えてしまいますorz

そこで、検索画面の一例として下記のようにしてみました。もっと良さげな方法があれば教えてください。

今回は、検索条件を入れると、検索画面が出てくる簡単な実装です。

DBは下記のような感じでデータが入ってます。

+----+-----------+---------------------+--------+
| id | shopid    | orderdate           | amount |
+----+-----------+---------------------+--------+
| 46 | testuser4 | 2008-08-01 00:00:00 |    100 | 
| 47 | testuser4 | 2008-08-01 00:00:00 |    200 | 
| 48 | testuser5 | 2008-08-01 00:00:00 |    300 | 
| 49 | testuser5 | 2008-08-01 00:00:00 |    400 | 
| 50 | testuser5 | 2008-08-01 00:00:00 |    500 | 
+----+-----------+---------------------+--------+


モデルはシンプルに下記のような感じ。

app/models/invoice.php

<?php
class Invoice extends AppModel {
	var $name = 'Invoice';
	var $useTable='invoices';

}

?>

検索画面のViewはこんな感じ。

app/views/invoices/search.ctp


<h2>
検索画面
</h2>

<?php echo $form->create(array("action" => "searchresult", "type" => "post"));?>

ショップID
<?php echo $form->text("Invoice.shopid", $options=array("size" => "40", "maxlength" => "40")); ?>

<br />

合計額
<?php echo $form->text("Invoice.amount", $options=array("size" => "10", "maxlength" => "10")); ?>
円以上

<?php echo $form->end('検索');?>

コントローラでは下記のように実装

app/controllers/invoices_controller.php

<?php
class InvoicesController extends AppController {

	var $name = 'Invoices';
	var $helpers = array('Html', 'Form', 'paginator');

	var $paginate = array("order" => array("Invoice.orderdate" => "desc"),
						  "limit" => 10
	);

	function search(){

	}

	function searchresult() {

		$this->Invoice->recursive = 0;

		if( !empty($this->data) ){
			$shopid = $this->data['Invoice']['shopid'];
			$amount = $this->data['Invoice']['amount'];
		}else{
			$shopid = $this->passedArgs['shopid'];
			$amount = $this->passedArgs['amount'];
		}

		$condition = array();
		$condition = array( "shopid like" => '%' . $shopid  .'%',
				    "amount >=" => $amount
		);

                /* 2008/12/25 下記にurlencodeを追加 */
		$searchword = array();
		$searchword = array( "shopid" => urlencode( $shopid ),
				     "amount" => urlencode( $amount )
		);

		$this->set('searchword', $searchword);
		$this->set('invoices', $this->paginate( $condition ));

	}

?>

/invoice/search/にアクセスするとsearch.ctpのviewを表示し、検索ボタンを押すと/invoice/searchresultに検索条件がPOSTされます。

コントローラのsearchresultでは、検索画面からPOSTされた時点では、$this->dataからPOSTデータを取得、検索結果画面でページングされる時は、POSTではなく、URLの中にパラメータが /invoice/searchresult/shopid:xxx/amount:100/page:2みたいに入ってくるので、URLの中のデータを取得する$this->passedArgs['shopid'];でデータを取得。

検索条件は、$conditionの配列で管理し、ページングのURLに入れる検索条件の値は、$searchwordの配列で管理します。

$searchwordの配列は、Viewに渡します。

2008/12/25追記

$searchwordのデータは、事前にurlencodeをかけておきます。そうしないとViewの中で展開されたurlに任意の文字列が注入できてしまうためです

Viewでは下記のようにoptions(array('url' => $searchword )); ?>という一文で検索条件の値をURLにセットします。そうすると、$paginator->sortや$paginator->nextのURLに自動的に検索条件のパラメータが/invoice/searchresult/shopid:xxx/amount:100/という感じでセットされます。

app/views/invoices/searchresult.ctp

<div class="invoices searchresult">
<h2>
検索結果
</h2>
<p>


<?php
echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%', true)
));
?></p>

<?php /* 2008/12/25修正 この例の$searchwordはArrayデータなのでStringを引数にとるurlencodeだとうまく動かないため、コントローラ側でurlencodeするように修正しました。 */ ?>
<?php $paginator->options(array('url' => $searchword  )); ?>


<table cellpadding="0" cellspacing="0">
<tr>
	<th><?php echo $paginator->sort('id');?></th>
	<th><?php echo $paginator->sort('売り上げ年月','orderdate');?></th>

	<th><?php echo $paginator->sort('ショップID','shopid');?></th>

	<th><?php echo $paginator->sort('合計金額','amount');?></th>
</tr>
<?php

foreach ($invoices as $invoice):

?>
	<tr>
		<td>
			<?php echo h( $invoice['Invoice']['id'] ); ?>
		</td>
		<td>
			<?php echo h( $invoice['Invoice']['orderdate']) ); ?>

		</td>

		<td>
			<?php echo h( $invoice['Invoice']['shopid'] ); ?>

		</td>

		<td>
			<?php echo h( $invoice['Invoice']['amount'] ); ?>
		</td>

	</tr>
<?php endforeach; ?>
</table>
</div>

<div class="paging">
	<?php echo $paginator->prev('<< '.__('previous', true), array(), null, array('class'=>'disabled'));?>
 | 	<?php echo $paginator->numbers();?>
	<?php echo $paginator->next(__('next', true).' >>', array(), null, array('class'=>'disabled'));?>
</div>


これで検索画面とその結果のページング処理が簡単にできるね!



追記

上記の例はフォームからのPOSTでしたが、id:j_okiさんがフォームからのGETリクエストの場合をまとめてくれてます。GJ!

http://d.hatena.ne.jp/j_oki/20080907/1220801326

2008-09-06

getで送信されたパラメータを引き継ぐpaginationのやり方

cakephp1.2RC2の環境を前提にしています。

例えば下記のように、

http://hogehoge.com/hoge/index/id1234

GETでid1234というパラメータ値を渡してそれを検索条件にしているような場合、viewのファイルに下記の1文を記述すればpaginationのURLにもパラメータが引き継がれます。

追記

紛らわしい書き方したみたいですみません。GETで送信されたというのは、フォームからのGETリクエストではなく、URLのリンクの中のパラメータという意味です。例えば、複数のショップの各月の売り上げ一覧画面があるとして、その一覧から特定ショップの各月の売り上げだけを見たいという場合に、一覧画面でショップ名の箇所などに特定ショップのショップIDをURLに含ませたリンク(invoice/index/shop123)を作成し、そこから遷移するようなものを下記の説明では想定してます。(bakeで作った画面で、editやviewへのリンクが/hoge/edit/2とかってなってますよね?そういったイメージです)

<?php $paginator->options(array('url' => $this->passedArgs)); ?>

上記の1文を書くと、$paginator->nextの箇所のURLは下記のように展開されます。

http://hogehoge.com/hoge/index/id1234/page:2

追記

passedArgsは、URLクエリの情報が格納されている配列なのですが、上記のようにそのまま渡してしまうと文字列がエスケープされないので、XSSの問題が発生します。事前にURLencodeするか、下記の記事を見てパッチを当ててください。

http://d.hatena.ne.jp/cakephper/20090226



下記に、これを使ったサンプルのコントローラとviewを記載します。

app/controllers/invoices_controller.php

<?php
class InvoicesController extends AppController {

	var $name = 'Invoices';
	var $helpers = array('Html', 'Form', 'paginator');

	function index( $shopid = null ) {

		$this->Invoice->recursive = 0;

		if( !empty($shopid) ){
			$condition = array( "shopid" => $shopid );

		}

		$this->set('invoices', $this->paginate( $condition ));
	}
}

viewは下記のファイルになります。

app/views/Invoices/index.ctp

<?php
echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%', true)
));
?></p>

<?php $paginator->options(array('url' => $this->passedArgs)); ?>


<table cellpadding="0" cellspacing="0">
<tr>
	<th><?php echo $paginator->sort('id');?></th>
	<th><?php echo $paginator->sort('ショップID','shopid');?></th>
	<th class="actions"><?php __('Actions');?></th>
</tr>

<?php
foreach ($invoices as $invoice):
?>
	<tr>
		<td>
			<?php echo $invoice['Invoice']['id']; ?>
		</td>

		<td>
			<?php echo h( $invoice['Invoice']['shopid'] ); ?>

		</td>


		<td class="actions">
			<?php echo $html->link(__('詳細', true), array('action'=>'view', $invoice['Invoice']['id'])); ?>
		</td>
	</tr>
<?php endforeach; ?>
</table>
</div>
<div class="paging">
	<?php echo $paginator->prev('<< '.__('previous', true), array(), null, array('class'=>'disabled'));?>
 | 	<?php echo $paginator->numbers();?>
	<?php echo $paginator->next(__('next', true).' >>', array(), null, array('class'=>'disabled'));?>
</div>