Jeans CMS 製作日記 / Security http://www.rad51.net/blog/jeans/ 自分のための次世代CMS製作メモ / セキュリティーメモ ja Jeans CMS © Weblog http://backend.userland.com/rss https://www.rad51.net/jeans/skins/jeans/images/jeans2.gif Jeans CMS 製作日記 http://www.rad51.net/blog/jeans/ 文字コードの問題 http://www.rad51.net/blog/jeans/?itemid=760
サポートする文字コードは、UTF-8のみにすることにした。これも、コードの簡潔化のため。その他、さまざまな規約を設けることで、簡潔なコードでの完成を目指す。今月は、Jeans月間かな?

で、表題の件。文字コードの問題は主に、XSS対策でのこと。2つ考え方があって、ひとつはJeansに入力される文字列をチェックする方法。もう一つは、出力するときにチェックする方法。

最初に書いたとおり、最初のバージョンではなるだけ全体のコードを簡潔にしたい。で、出力側にはhtmlspecialchars()を通すのだが、ここでUTF-8エンコード指定がどのように考慮されているのかを調べてみた。

<?php

show('試験の文章(SJIS)');
show('Alphabet only.');
ob_start();
show(mb_convert_encoding('試験の文章(UTF-8)','UTF-8','SJIS'));
echo mb_convert_encoding(ob_get_clean(),'SJIS','UTF-8');

function show($text){
    $text=htmlspecialchars($text,ENT_QUOTES,'UTF-8');
    echo "'$text'(".strlen($text).")";
    echo rawurlencode($text);
    echo "\n-----------------------\n";
}

Windowsのコンソールからこのコードを実行すると、次のようになる。

''(0)
-----------------------
'Alphabet only.'(14)Alphabet%20only.
-----------------------
'試験の文章(UTF-8)'(22)%E8%A9%A6%E9%A8%93%E3%81%AE%E6%96%87%E7%AB%A0%28UTF-8%29
-----------------------

SJISの文字列をUTF-8でhtmlspecialchars()に通すと、空の文字列が帰ってきた。とりあえず、最初のバージョンでは、出力側のチェックとして、htmlspecialchars()を通すだけでよさそうだ。]]>
Security http://www.rad51.net/blog/jeans/?itemid=760 Tue, 03 Nov 2009 14:34:35 PST ITEM760_20091103
restriction::restrictHtmlTags()メソッド http://www.rad51.net/blog/jeans/?itemid=671 複数ユーザで使用時のアイテムパースでの脆弱性の回避について、結論が出せた。

やはり、ホワイトリストで対処するのが一番確実なやり方なので、利用可能なタグをホワイトリストで用意し、それに該当しないものはずべてhtmlspecialchars()互換の方法で無効化することにした。

複数ユーザで使用しているときの制限に関するメソッドを集めるためのクラスを一つ作成し、ここに目的のメソッドを配置(restriction::restrictHtmlTags())した。

<?php
class restriction {
    static private $allowHtmlTags=array();
    static public function init(){
        self::$allowHtmlTags['tags']='a|address|area|b|basefont|bgsound|big|blink|blockquote|br|caption|center|cite|code|colgroup'.
            '|dd|del|div|dl|dt|em|fieldset|font|h1|h2|h3|h4|h5|h6|hr|i|img|ins|label|legend|li|map|marquee|nobr|ol|p|pre|q|rb|rp|rt|ruby|s|samp|small'.
            '|span|strike|strong|sub|sup|table|tbody|td|tfoot|th|thead|tr|tt|u|ul|var';
        self::$allowHtmlTags['props']='align|alt|background|balance|bgcolor|border|bordercolor|bordercolordark|bordercolorlight|class|cellpadding|cellspacing'.
            '|color|compact|cite|clear|face|for|frame|gallervimg|height|href|hspace|id|loop|name|noshade|rules|size|span|src|start|summary|target|type|usemap|value|volume|vspace|width|';
        self::$allowHtmlTags['pattern']=array(
            '!&lt;(/?)(<%tags%>)(([\s]+(<%props%>)="(http:|https:|ftp:|[^":])[^":]*")*)([\s]*/?)&gt;!i'=>'<$1$2$3$7>',
            '!&lt;%([\s\S]*?)%&gt;!'=>'<%$1%>',
            '!&lt;--([\s\S]*?)--&gt;!'=>'<!--$1-->'
        );
    }
    static public function allowHtmlTags($tags=0, $props=0, $patterns=0){
        static $search=0,$replace=0;
        // When the paramers are not given, return the cached values
        if ($search && $replace && !($tags||$props||$patterns)) return array($search,$replace);
        // When the paramers are given, change the setting.
        if ($tags) {
            if (is_array($tags)) $tags=implode('|',$tags);
            if (preg_match('/[\|a-z]/i',$tags)) self::$allowHtmlTags['tags'].='|'.$tags;
        }
        if ($props) {
            if (is_array($pros)) $pros=implode('|',$pros);
            if (preg_match('/[\|a-z]/i',$pros)) self::$allowHtmlTags['pros'].='|'.$pros;
        }
        if ($patterns) {
            foreach($patterns as $key=>$value) self::$allowHtmlTags['pattern'][$key]=$value;
        }
        $search=$replace=array();
        foreach(self::$allowHtmlTags['pattern'] as $key=>$value){
            $search[]=str_replace(
                array('<%tags%>','<%props%>'),
                array(self::$allowHtmlTags['tags'],self::$allowHtmlTags['props']),
                $key);
            $replace[]=$value;
        }
        // Return the values, finally.
        return array($search,$replace);
    }
    static public function restrictHtmlTags($html){
        list($search,$replace)=self::allowHtmlTags();
        $html=str_replace(array('&','<','>'),array('&amp;','&lt;','&gt;'),$html);
        $html=preg_replace($search,$replace,$html);
        $html=str_replace('&amp;','&',$html);
        return $html;
    }
    
}

scriptタグなどは当然ながら、含まれていない。また、hrefやsrc, width, heightなど、タグにおけるプロパティ指定についても、ホワイトリストで指定することにした。当然ながら、onclickやonmouseoverなどのスクリプト関連は入っていない。また、""で囲まれた値指定の部分で『:』を利用する際は、『http:』『https:』『ftp:』の3つだけが使えるようにした。これは、『javascript:』などの記述が使えないようにするため。

同じやり方は、Nucleusにも使えるはず。superadminでないユーザの書いた記事はすべてこのメソッドを通せばよい。また、superadminでないユーザのPOST値すべてについてこのメソッドで処理すると、blog-adminがブログの設定値(短縮名・説明・URLなど)にスクリプトを仕込んだり出来なくなるはず(安全なタグは記述できる)。

なお、<div style="text-align: center">などのスタイルシート直接指定は、デフォルトでは禁止されている。これは、IEではスタイルシートでスクリプトが使えるので禁止しないといけないため。ただし、タグでのクラス指定は出来るので、別途用意したCSSでクラスごとのスタイルを指定しておけば、タグでそれらのスタイルを呼び出すことは可能。

NP_0NoScriptByUser???]]>
Security http://www.rad51.net/blog/jeans/?itemid=671 Sun, 27 Jul 2008 18:16:06 PDT ITEM671_20080727
itemをevalするのはまずいかも http://www.rad51.net/blog/jeans/?itemid=660
先日、とあるPHPの本を読んで知ったのだが、HTMLへのPHPコードの挿入は、<?php ... ?>以外に、<script language="php">...</script>でもできる。PHP5でもこれを確認した(PHP6ではどうだか知らない)。

現在のバージョンでは、それぞれのアイテムのパースにもevalを使っている。admin以外でのPHPコードの実行を回避するために、『<?』を『&lt;?』に変換しているが、これだけでは穴があるということになる。

改めて、ブラックリスト形式でのセキュリティー対策が怖いことを思い知らされた。ここの部分の実装を再考せねばならない。ここの対策は、前から気になっていたところではある。

1)いっそのこと、メンバーは一人だけ(superadmin)にする

2)superadmin以外の記事はすべてhtmlspecialcharsし、ホワイトリスト形式で、許されたタグだけ元のタグに戻す

3)superadmin以外の記事はすべてhtmlspecialcharsし、別途BBcodeなどをコアでサポートする

などの対策が考えられる。このツールの開発を始めた当初は1)も考えたが、多分この方法は取らない。Nucleusにおける複数メンバーでの使用の利便性を考えると、superadmin以外はすべてhtmlspecialcharsにしてでも複数メンバーに対応したほうがよさそう。

ならば、2)か3)だが、多分コアでは2)をサポートすることになると思う。とりあえず、正規表現を考えてみる。

どちらの方法を使うにせよ、htmlspecialcharsの後『&lt;%』を『<%』に戻してからevalし、最後にタグに変換するのがよさそう。そのため、『<%』ではPHPコードが実行されないサーバ設定であることを確認するルーチンを入れる必要がある(もっとも、そうなっていないとほとんどのスキン変数でエラーが出るのだけれど)。]]>
Security http://www.rad51.net/blog/jeans/?itemid=660 Wed, 25 Jun 2008 14:47:14 PDT ITEM660_20080625
セキュリティメモ http://www.rad51.net/blog/jeans/?itemid=651
さて、Jeansの骨格部分はあらかた仕上がっているので、ここでもう一度セキュリティーをチェックするため、『PHPサイバーテロの技法』を読んで、問題(になるかも知れない)部分をピックアップしてみた。

SQLインジェクション対策

SQLクエリーを作成するためのsql::fill()メソッドに問題がありそうだ。数値かどうかをis_numeric()で判断して条件分岐しているが、ここは『is_int() || is_float()』で分岐する必要がある。それに伴って、一部のコードを書き換える必要があるかも知れない。また、core::checkRequest()でのチェックだが、リクエスト名が『id』で終わる場合に、$request=(int)$request; などとしたほうが良いかも知れない(ここの部分は、実際にリクエストをどのようにクエリーに渡しているかの実例を見ながら判断)。

ヌルバイト攻撃対策

ヌルバイトについては、core::checkRequest()メソッドで入力値のチェックを行っているが、念のため、jeans::checkFile()メソッドでもチェックした方がよさそう。あと、ereg()系の関数を使っていないかどうか、再チェック(私はpregの方が好きなので、使っていないはず)。

HTTPヘッダ周り

head()関数をラップして、改行(CRLF)を引数に取れないようなメソッドを、コアに用意。

ファイルアップロード関連

ここは、未実装。実装の際は、拡張子のチェックをホワイトリストで行うこと。拡張子がイメージの場合、getimagesize()でチェックする。

XSS対策

<a href="xxxx">のxxxxの部分に挿入するためのサニタイジングメソッドを、コアに用意。

$_SERVER

現在の所、とりあえず REQUEST_URI と HTTP_REFERER のチェックは行っているが、もう少しチェックルーチンを追加した方がよさそう。


ざっと見たところ、こんな感じ。最初のベータバージョンをリリースする前に、再チェックする。]]>
Security http://www.rad51.net/blog/jeans/?itemid=651 Wed, 14 May 2008 18:38:56 PDT ITEM651_20080514
httpOnly http://www.rad51.net/blog/jeans/?itemid=641
さて、webサーフィンしていて見つけたセキュリティー情報のメモ。

httpOnlyをFirefoxで
PHP 5.2でクッキーのhttpOnlyフラグがサポートされるみたいです

XSSによるクッキー情報漏えいの防止策の一つとして。あとで、Jeansにも導入する。]]>
Security http://www.rad51.net/blog/jeans/?itemid=641 Thu, 17 Apr 2008 19:42:29 PDT ITEM641_20080417
セキュリティー対策、あれこれ http://www.rad51.net/blog/jeans/?itemid=593
1)SQLインジェクション対策
 sql::query()メソッドで、簡易プリペアードステートメントを使う。MySQL5やPostgreSQLほど本格的なプリペアードステートメントではないが、この方法でほぼ100%、SQLインジェクションは防げる。SQLステートメントでフィールド部分を入力値にしたがって変更したい場合は、core::fill()メソッドを用いて正規表現でホワイトリスト形式で入力値をチェックしながら値を挿入する。

2)XSS対策
 core::echohtml()メソッドで、プリペアードステートメント様の機能が使える。正規表現で入力値をチェックすることも可能。

3)リモートスクリプトインクルード対策
 クラス名を適切に選んでおけば、PHPファイルのインクルードはコアが自動的に行ってくれる。include, require などのステートメントを使うことはほとんどないはず。

4)ディレクトリトラバーサル対策
 file_exists()の代わりに、core::checkFile()メソッドを使用する(下のコードを参照)。

5)ヌルバイト攻撃対策
 リクエストにヌルバイトがあると、コア呼び出し時に自動的に停止する(先の記事のコードを参照)。

coreクラス
static public function checkFile($dir,$file){
    if (strpos($fullpath=realpath($dir.$file),realpath($dir))===0) return file_exists($fullpath);
    exit('Are you trying the directory traversal?');
}
]]>
Security http://www.rad51.net/blog/jeans/?itemid=593 Sun, 03 Feb 2008 16:51:25 PST ITEM593_20080203
セキュリティー対策 http://www.rad51.net/blog/jeans/?itemid=535
1)リモートコードインサーション対策
2)XSS対策
3)SQLインジェクション対策

何も考えずにコアの機能を利用することで、これらのセキュリティー対策が行えるようなツールを目指す。理想としては、意識せずにコードを書けば脆弱性はでず、何か特別なことを仕様としたときだけに脆弱性が出てしまうようなものにしたい。規約として、echo 命令はライブラリやプラグインでは使用禁止にするつもり。

1)リモートコードインサーション対策

 __autoload()関数を活用することで、ほとんどのケースにおいて include() require() などの関数を利用することは無いはず。__autoload()周りは、いまのところ次のとおり。

// There is only one global function.
function __autoload($className) {
    core::autoload($className);
}
class core {
    static function autoload($className){
        switch(substr($className,0,3)){
        case 'jp_': // plugins
            if (file_exists($file=DIR_PLUGINS.$className.'.php')) {
                require_once($file);
            } else exit ('Class PHP file not found.');
            break;
        default:
            if (file_exists($file=DIR_LIBS.$className.'.php')) {
                require_once($file); // libraries
            } elseif (file_exists($file=$className.'.php')) {
                require_once($file); // file in current directory.
            } else exit ('Class PHP file not found.');
            break;
        }
        // Call init() method if exists.
        @call_user_func(array($className,'init'));
    }

クラスが必要なときは、自動的に jeans/libs ディレクトリもしくはプラグインディレクトリから呼び出される。

2)XSS対策

 コアに、次のようなメソッドを用意した。

class core {
    static function p(){
        foreach(func_get_args() as $text){
            echo htmlspecialchars($text,ENT_QUOTES,_CHARSET);
        }
    }
    static function fill(string $html,$data){
        if (!is_array($data)) $data=array($data);
        $search=array();
        $replace=array();
        foreach($data as $key=>$value){
            $search[]='<%'.(is_integer($key)?$key+1:$key).'%>';
            $replace[]=htmlspecialchars($value,ENT_QUOTES,_CHARSET);
        }
        return str_replace($search,$replace,$html);
    }
    static function echohtml(string $html,$data){
        echo self::fill($html,$data);
    }

 htmlspecialcharsを通して出力したい場合は、core::p()メソッドを用いればよい。毎回『htmlspecialchars, ENT_QUOTES, _CHARSET』を打ち込む必要が無いので、プログラミングもずいぶん楽になるはず。また、HTMLタグを含めた出力には、core::echohtml()メソッドを用いる。2番目の引数に与えたデータは、htmlspecialcharsを通して挿入され、表示される。例えば、次のように記述する。
core::echohtml('<b>ID: <%1%>, title:<%2%></b>',array($itemid,$title));


3)SQLインジェクション対策

sqlクラスに、次のメソッドを用意。

class sql {
    static public function query ($query,$data=null){
        if ($data!==null) $query=self::fill($query,$data);
        self::checkQuery($query);
        $res=sqlite_query (self::$obj->db,$query);
        return $res;
    }
    static function fill($query,$data){
        if (!is_array($data)) $data=array($data);
        $search=array();
        $replace=array();
        foreach($data as $key=>$value){
            $search[]='<%'.(is_integer($key)?$key+1:$key).'%>';
            if (is_integer($value)) {
                $replace[]=(int)$value;
            } elseif(is_numeric($value)) {
                $replace[]=(float)$value;
            } else {
                $replace[]="'".sqlite_escape_string($value)."'";
            }
        }
        return str_replace($search,$replace,$query);
    }
    static private function checkQuery($query){
        $query=preg_replace('/;[\s]*$/','',$query); // remove last ';';
        if (strpos($query,';')===false) return; // if there isn't ';', OK.
        foreach(preg_split("/'/",$query) as $key=>$value){
            if ($key%2==1) continue; // ignore inside string
            if (strpos($value,';')===false) continue; // if there isn't ';', OK.
            exit('Cannot divide SQL query by ";"');
        }
    }

 sql::query()メソッドを呼び出すとき、2番目の引数にデータを与えれば、自動的にサニタイズされたデータが挿入される仕組み。sqlite_escape_stringなどを使う必要が無い。例えば、次のように記述する。
$res=sql::query('SELECT * FROM jeans_item WHERE category=<%1%> AND title=<%2%>', array($itemid,$title));

 なお、sql::checkQuery()メソッドは現在のところ、『 ; 』を用いた2つ以上のクエリーを実行することを禁止するために用いている。]]>
Security http://www.rad51.net/blog/jeans/?itemid=535 Mon, 24 Dec 2007 13:44:45 PST ITEM535_20071224