2011-01-03 11 views
3

私が開発したサイトは、恐らくブルートフォース攻撃やレインボーテーブル攻撃の可能性があります。元のログインスクリプトにはSALTがなく、パスワードはMD5に保存されていました。このPHPログインスクリプトを批判してください。

以下は、SALTとIPアドレスの禁止を含む更新されたスクリプトです。さらに、同じIPアドレスまたはアカウントでログインが失敗した場合、Maydayの電子メールとSMSを送信し、アカウントを無効にします。それを見て、何が改善できるのか、欠けているのか、それほど単純で奇妙なことを教えてください。

<?php 
    //Start session 
    session_start(); 
    //Include DB config 
    include $_SERVER['DOCUMENT_ROOT'] . '/includes/pdo_conn.inc.php'; 

    //Error message array 
    $errmsg_arr = array(); 
    $errflag = false; 

    //Function to sanitize values received from the form. Prevents SQL injection 
    function clean($str) { 
     $str = @trim($str); 
     if(get_magic_quotes_gpc()) { 
      $str = stripslashes($str); 
     } 
     return $str; 
    } 

    //Define a SALT, the one here is for demo 
    define('SALT', '63Yf5QNA'); 

    //Sanitize the POST values 
    $login = clean($_POST['login']); 
    $password = clean($_POST['password']); 
    //Encrypt password 
    $encryptedPassword = md5(SALT . $password); 
    //Input Validations 
    //Obtain IP address and check for past failed attempts 
    $ip_address = $_SERVER['REMOTE_ADDR']; 
    $checkIPBan = $db->prepare("SELECT COUNT(*) FROM ip_ban WHERE ipAddr = ? OR login = ?"); 
    $checkIPBan->execute(array($ip_address, $login)); 
    $numAttempts = $checkIPBan->fetchColumn(); 
    //If there are 4 failed attempts, send back to login and temporarily ban IP address 
    if ($numAttempts == 1) { 
     $getTotalAttempts = $db->prepare("SELECT attempts FROM ip_ban WHERE ipAddr = ? OR login = ?"); 
     $getTotalAttempts->execute(array($ip_address, $login)); 
     $totalAttempts = $getTotalAttempts->fetch(); 
     $totalAttempts = $totalAttempts['attempts']; 
     if ($totalAttempts >= 4) { 
      //Send Mayday SMS 
      $to = "[email protected]"; 
      $subject = "Banned Account - $login"; 
      $mailheaders = 'From: [email protected]' . "\r\n"; 
      $mailheaders .= 'Reply-To: [email protected]' . "\r\n"; 
      $mailheaders .= 'MIME-Version: 1.0' . "\r\n"; 
      $mailheaders .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n"; 
      $msg = "<p>IP Address - " . $ip_address . ", Username - " . $login . "</p>"; 
      mail($to, $subject, $msg, $mailheaders); 
      $setAccountBan = $db->query("UPDATE ip_ban SET isBanned = 1 WHERE ipAddr = '$ip_address'"); 
      $setAccountBan->execute(); 
      $errmsg_arr[] = 'Too Many Login Attempts'; 
      $errflag = true;  
     } 
    } 
    if($login == '') { 
     $errmsg_arr[] = 'Login ID missing'; 
     $errflag = true; 
    } 
    if($password == '') { 
     $errmsg_arr[] = 'Password missing'; 
     $errflag = true; 
    } 

    //If there are input validations, redirect back to the login form 
    if($errflag) { 
     $_SESSION['ERRMSG_ARR'] = $errmsg_arr; 
     session_write_close(); 
     header('Location: http://somewhere.com/login.php'); 
     exit(); 
    } 

    //Query database 
    $loginSQL = $db->prepare("SELECT password FROM user_control WHERE username = ?"); 
    $loginSQL->execute(array($login)); 
    $loginResult = $loginSQL->fetch(); 

    //Compare passwords 
    if($loginResult['password'] == $encryptedPassword) { 
     //Login Successful 
     session_regenerate_id(); 
     //Collect details about user and assign session details 
     $getMemDetails = $db->prepare("SELECT * FROM user_control WHERE username = ?"); 
     $getMemDetails->execute(array($login)); 
     $member = $getMemDetails->fetch(); 
     $_SESSION['SESS_MEMBER_ID'] = $member['user_id']; 
     $_SESSION['SESS_USERNAME'] = $member['username']; 
     $_SESSION['SESS_FIRST_NAME'] = $member['name_f']; 
     $_SESSION['SESS_LAST_NAME'] = $member['name_l']; 
     $_SESSION['SESS_STATUS'] = $member['status']; 
     $_SESSION['SESS_LEVEL'] = $member['level']; 
     //Get Last Login 
     $_SESSION['SESS_LAST_LOGIN'] = $member['lastLogin']; 
     //Set Last Login info 
     $updateLog = $db->prepare("UPDATE user_control SET lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR), ip_addr = ? WHERE user_id = ?"); 
     $updateLog->execute(array($ip_address, $member['user_id'])); 
     session_write_close(); 
     //If there are past failed log-in attempts, delete old entries 
     if ($numAttempts > 0) { 
      //Past failed log-ins from this IP address. Delete old entries 
      $deleteIPBan = $db->prepare("DELETE FROM ip_ban WHERE ipAddr = ?"); 
      $deleteIPBan->execute(array($ip_address)); 
     } 
     if ($member['level'] != "3" || $member['status'] == "Suspended") { 
      header("location: http://somewhere.com"); 
     } else { 
      header('Location: http://somewhere.com'); 
     } 
     exit(); 
    } else { 
     //Login failed. Add IP address and other details to ban table 
     if ($numAttempts < 1) { 
     //Add a new entry to IP Ban table 
     $addBanEntry = $db->prepare("INSERT INTO ip_ban (ipAddr, login, attempts) VALUES (?,?,?)"); 
     $addBanEntry->execute(array($ip_address, $login, 1)); 
     } else { 
      //increment Attempts count 
      $updateBanEntry = $db->prepare("UPDATE ip_ban SET ipAddr = ?, login = ?, attempts = attempts+1 WHERE ipAddr = ? OR login = ?"); 
      $updateBanEntry->execute(array($ip_address, $login, $ip_address, $login)); 
     } 
     header('Location: http://somewhere.com/login.php'); 
     exit(); 
    } 
?> 

EDIT

さて、ここではランダムな塩で私の試みです。記憶されたランダムに生成された塩を使用して格納されたハッシュ値を比較し、ユーザがログインすると

$hash = hash('sha256', $salt . $pw); //$pw is the cleaned user submitted password 

:ランダム塩とのハッシュを生成しそして

define('SALT_LENGTH', 15); 
function createSalt() 
{ 
    $key = '[email protected]#$%^&*()_+=-{}][;";/?<>.,'; 
    $salt = substr(hash('sha512',uniqid(rand(), true).$key.microtime()), 0, SALT_LENGTH); 
    return $salt; 
} 
$salt = createSalt() 
//More prep for entering into table... 

:まず、テーブルに挿入される塩を作成します:

$loginHash = hash('sha256', $dbSalt . $pw); 
if ($loginHash == $dbHash) { 
    //Logged in 
} else { 
    //Failed 
} 

どのように見えますか?

+0

また、これが10未満の人が使用するカスタムCMSシステムのためのものであることを追加する必要があります。ログインは非常に頻繁にではなく、禁止は、個人への電話の後すぐに解除されます – NightMICU

答えて

8

[OK]を、ここではいくつかです:あなたはもうmd5を使用すべきではない

  1. ですが、より良い方法があります(はhash()の関数で使用されています)。

  2. 私はもっと長い静的塩も使用します。少なくとも64文字をお勧めします(結局のところ、書き込みで計算するための最小限のオーバーヘッドだが、推測するのがはるかに難しい)。

  3. また、動的(ランダム)塩も追加します。ユーザーごとに新しいパスワードを生成し、パスワードハッシュの横に格納します(パスワードは:文字で区切るのが一般的です)。この方法では、たとえ静的な塩が漏れたとしても、データベース内の各パスワードについて、新しい虹のテーブルを生成する必要があります(または少なくとも繰り返します)。

  4. IP非臨時の住所。ほとんどのISPは、1つのIPから2人以上のユーザーが見えるような形式のNATを使用しています(IPv4ネームスペースの枯渇によりますます普及するでしょう)。 IPアドレスをレート制限または一時的にブロックする場合は、罰金がかかります。しかし、それらを禁止しないでください...

  5. clean()関数は文字列を最初にチェックするか、文字列(:$str = is_string($str) ? trim($str) : (string) $str;)にする必要があります。それはSQLインジェクションをまったく防ぐものではありません。しかし、magic_quotes_gpcが設定されている(あなたのために引用符をエスケープしようとする)サーバ上でコードを動作させるには、stripslashesコールが必要です。

  6. コードをよりよくフォーマットします。関連タスクを処理する関数を作成します。そうすれば、何が起きているのかを把握するための75行の手続き型コードはありません。もっと良い方法は、クラスにラップし、共通のタスク(dbアクセスなど)を自分のクラスに移動することです。そして、適切にインデントすることを忘れないでください。可読性は王であるので、ショートカットを取ることはありません...

編集:は限りパスワードを検証する方法として、あなたが最初に塩漬けハッシュを取得、保存された塩と、その後、再計算したハッシュ。 (私は以下を示しmakeSaltedHash機能はKey Stretchingと呼ばれる何かを付加的に使用します。

function validatePassword($password, $hash) { 
    list($oldHash, $salt) = explode(':', $hash, 2); 
    $newHash = makeSaltedHash($password, $salt); 
    return $hash == $newHash; 
} 

function makeSaltedHash($password, $salt = '') { 
    if (empty($salt)) { 
     $salt = makeRandomSalt(mt_rand(64, 128)); 
    } 
    $hash = hash('sha512', $password . $salt . SALT); 
    for ($i = 0; $i < 50; $i++) { 
     $hash = hash('sha512', $password . $salt . SALT . $hash); 
    } 
    return $hash . ':' . $salt; 
} 

function makeRandomSalt($length = 64) { 
    $salt = ''; 
    for ($i = 0; $i < $lenght; $i++) { 
     $salt .= chr(mt_rand(33, 126)); 
    } 
    return $salt; 
} 
+0

塩はテーブルの同じ列にパスワードsalt:パスワードとして保存されますか?これは、私が本当にぼやけているところです。ユーザーがアカウントを作成したりパスワードを変更したときにSALTがランダムに生成された場合、ログイン時に検証する方法です。 – NightMICU

+0

@ NightMICU:そうできる。別の列などに保存することができます。擬似コードでこの種のものを検証する方法については、私の編集を参照してください。 – ircmaxell

+0

@NightMICUこれらの塩関連ポイントのほとんどはちょうどうそつきです。まず第一に、全世界の誰もこのハッシュを気にしません。誰もそれらを必要としない。 –

-5

clean()には、mysql_real_escape_string()を追加する必要があります。

サーバーやその他の設定によっては、$_SERVER['REMOTE_ADDR']は空にすることができます。

header()リダイレクト後にexitを忘れた場合のエラーを防ぐために、簡単なリダイレクト機能を作成することをお勧めします。

例:

function redirect($url) { 
    if (!headers_sent()) { 
    header('Location: '.$url); 
    } else { 
    // echo $url; 
    } 
    exit; 
} 
+0

私はstripslashesも少し古くなっていると思います – Nick

+0

バインドされたパラメータを使用することで、エスケープはPDOによって行われますmysql_real_escape_string() 'は不要なスラッシュを追加し、 -1 –

+0

私は、元のバージョンがPDOプリペアドステートメントを使用していないことを追加する必要があります。このアップグレードを考慮してclean()関数を削除する必要がありますか?おそらく冗長ですか? – NightMICU

1
  1. あなたはもう、 "安全" とはみなされませんMD5を、使用しています。

  2. 本当に長い塩が必要です。

塩析法を有効に使用するには、see this questionをご使用ください。

+0

私はSALTを長くする必要があると思っていました。デモ用です。長さを示唆できますか?私は辞書の単語を避けることがベストプラクティスであると推測しています。 MD5については、代わりにsha1()を使うべきですか? – NightMICU

+0

@NightMICU私の答えを更新しました。参照される質問では、 'hash()'関数を探します。 –

+0

@Night 'hash( 'sha256'、$ yourstring);を使用します。 '。それはsha1よりも良いです – JakeParis

0

2回失敗した後にrecaptchaを入れてください。しかし、本当にmemcacheを使ってipを保存したいのであれば禁止してください。 set($ ip、true、false、$ secondsTTL);後でget($ ip)でチェックしてください - TTLを2時間に設定してください。 また、関数のすべてを入れて、文字列引用や二重引用符ではなく、あなたが使いたいものを見つけることができます。 ;)

すべてが一緒になって動作するように見えますが、実際には読みにくく、いくつかのものは冗長です。私から

+0

私はほとんどの場合禁止に同意しますが、これは約10のアカウントを持つ小さなCMSシステムであり、私はすべての連絡先情報を持っています。 recaptchaは悪い考えではありませんが、私はそれについて考えてみたいです! – NightMICU

4

つのヒント:

  • は、SQLインジェクションを防ぐためにstripslashesやマジッククオートを使用しないでください。 PDOパラメータを使用します。例外なく。
  • ユーザーごとに異なる塩を使用してください。塩は秘密ではなく、ユーザーレコードとともにDBに保管します。誰もが同じ塩を使用すると、データベースがより攻撃的になります。

無関係ですが、よく見落とされます。ユーザーのパスワードの長さを制限しないでください。私は、パスワードの長さに任意の制限(12文字のような)を課しているあまりにも多くのWebサイトを見てきましたが、奇妙な複雑さのルールを持っています("少なくとも1つ上、下1つ、数字と特殊文字は '<' 、 '>' "またはそのようなナンセンス)。これは非常に敵対的で、避けてください。

+0

+1良いヒント。 –

+0

確かに良いヒント。ですから、私はPDOプリペアドステートメントを使用しているのでclean()関数を取り除かなければなりませんか?そして、はい、私はパスワードのルールに同意します - それは私が練習しないものです。 – NightMICU

+0

@NightMICU:サーバー環境を制御し、 'magic_quotes_gpc'を無効にできない限り、' clean'機能を削除しないでください。あなたができない場合、あなたはまだその機能を持っている必要があります... – ircmaxell

関連する問題