私のWebアプリケーションは、セッションを使用して、ログインしたユーザーに関する情報を保存し、ユーザーがアプリ内でページ間を移動するときにその情報を維持します。この特定のアプリケーションでは、個人のuser_id
、first_name
、last_name
を保存しています。
ログイン時に「ログイン状態を維持する」オプションを提供して、ユーザーのマシンに2週間Cookieを置き、アプリに戻ったときに同じ詳細でセッションを再開するようにします。
これを行うための最良のアプローチは何ですか? Cookieにuser_id
を保存したくないのは、あるユーザーが別のユーザーのIDを簡単に偽造しようとするからです。
はっきり言って、ユーザーデータ、またはユーザーデータから派生したものをこの目的のためにCookieに入れている場合、何か間違ったことをしていることになります。
そこ。言った。これで、実際の答えに移ることができます。
ユーザーデータのハッシュの何が問題になっていますか?まあ、それは露出面と隠蔽によるセキュリティに帰着します。
あなたが攻撃者であると想像してみてください。セッションの記憶に設定された暗号化Cookieが表示されます。幅は32文字です。うんそれはMD5かもしれません...
また、彼らがあなたが使用したアルゴリズムを知っていることを想像してみましょう。例えば:
md5(salt+username+ip+salt)
これで、攻撃者が行う必要があるのは、「塩」をブルートフォースすることです(これは実際には塩ではありませんが、後で詳しく説明します)。IPアドレスの任意のユーザー名で必要なすべての偽トークンを生成できます!しかし、塩を強引に強制するのは難しいですよね?絶対に。しかし、現代のGPUは非常に優れています。そして、あなたがそれに十分なランダム性を使用しない限り(十分に大きくする)、それはすぐに落ちて、それであなたの城の鍵になります。
要するに、あなたを保護しているのは塩だけです。それはあなたが思っているほどあなたを本当に保護しているわけではありません。
しかし、待って!
これらはすべて、攻撃者がアルゴリズムを知っていることを前提としています!それが秘密で混乱しているなら、あなたは安全ですよね? 間違っている。その考え方には名前があります:Security Through Obscurity、これはNEVERに依存するべきではありません。
より良い方法
より良い方法は、idを除き、ユーザーの情報がサーバーから決して出ないようにすることです。
ユーザーがログインしたら、大きな(128〜256ビット)ランダムトークンを生成します。トークンをユーザーIDにマップするデータベーステーブルにそれを追加し、Cookieでクライアントに送信します。
攻撃者が別のユーザーのランダムトークンを推測した場合はどうなりますか?
さて、ここで計算をしましょう。 128ビットのランダムトークンを生成しています。それは以下があることを意味します:
possibilities = 2^128
possibilities = 3.4 * 10^38
さて、その数がどれほど馬鹿げているかを示すために、インターネット上のすべてのサーバー(今日は50,000,000としましょう)が、毎秒1,000,000,000の割合でその数を総当たりしようとしていると想像してみましょう。実際には、サーバーはそのような負荷の下で溶けますが、これを試してみましょう。
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
毎秒50兆回の推測です。速い!右?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
6.8セクシリオン秒...
それをもっとわかりやすい数字にまとめてみましょう。
215,626,585,489,599 years
またはさらに良い:
47917 times the age of the universe
はい、それは宇宙の年齢の47917倍です...
基本的に、クラックされることはありません。
まとめると:
私がお勧めするより良い方法は、3つの部分でCookieを保存することです。
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
次に、検証するには:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
注:データベース内のレコードを検索するために、トークンまたはユーザーとトークンの組み合わせを使用しないでください。必ずユーザーに基づいてレコードをフェッチし、タイミングセーフな比較関数を使用して、フェッチしたトークンを後で比較してください。 タイミング攻撃の詳細 。
現在、veryは、SECRET_KEY
が暗号の秘密であることが重要です(/dev/urandom
のようなものによって生成され、および/または高エントロピー入力から派生)。また、GenerateRandomToken()
は強力なランダムソースである必要があります(mt_Rand()
は十分に強力ではありません。 RandomLib や random_compat 、またはDEV_URANDOM
を使用したmcrypt_create_iv()
などのライブラリを使用してください)...
hash_equals()
は、 タイミング攻撃 を防ぐためのものです。 PHP 5.6より下のPHPバージョンを使用する場合、関数 hash_equals()
はサポートされません。この場合、 hash_equals()
をTimingSafeCompare関数に置き換えることができます。
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
セキュリティ通知:決定論的データのMD5ハッシュからCookieを作成することは悪い考えです。 CSPRNGから派生したランダムトークンを使用することをお勧めします。より安全なアプローチについては、この質問に対する ircmaxellの答え を参照してください。
通常、私はこのようなことをします:
もちろん、異なるCookie名などを使用することもできます。また、Cookieの内容を少し変更することもできます。簡単に作成できないようにしてください。たとえば、ユーザーが作成されたときにuser_saltを作成し、それをcookieに入れることもできます。
また、md5(またはほとんどすべてのアルゴリズム)の代わりにsha1を使用できます。
はじめに
あなたの肩書き「ログイン状態を保持」-最善のアプローチで、どこから始めればよいかわかりにくくなります。以下を考慮する必要があります。
Cookies
Cookieは脆弱です。一般的なブラウザのCookie盗難の脆弱性とクロスサイトスクリプティング攻撃の間では、Cookieが安全でないことを受け入れる必要があります。セキュリティを向上させるには、php
setcookies
に次のような追加機能があることに注意する必要があります。
bool setcookie (string $ name [、string $ value [、int $ expire = 0 [、string $ path [、string $ domain [、bool$ secure= false [、bool$ httponly= false]]]]]])
定義
シンプルなアプローチ
簡単な解決策は次のとおりです。
上記のケーススタディでは、このページに記載されているすべての例をまとめていますが、欠点は次のとおりです。
より良い解決策
より良い解決策は
サンプルコード
// Set privateKey
// This should be saved securely
$key = 'fc4d57ed55a78de1a7b31e711866ef5a2848442349f52cd470008f6d30d47282';
$key = pack("H*", $key); // They key is used in binary form
// Am Using Memecahe as Sample Database
$db = new Memcache();
$db->addserver("127.0.0.1");
try {
// Start Remember Me
$rememberMe = new RememberMe($key);
$rememberMe->setDB($db); // set example database
// Check if remember me is present
if ($data = $rememberMe->auth()) {
printf("Returning User %s\n", $data['user']);
// Limit Acces Level
// Disable Change of password and private information etc
} else {
// Sample user
$user = "baba";
// Do normal login
$rememberMe->remember($user);
printf("New Account %s\n", $user);
}
} catch (Exception $e) {
printf("#Error %s\n", $e->getMessage());
}
使用クラス
class RememberMe {
private $key = null;
private $db;
function __construct($privatekey) {
$this->key = $privatekey;
}
public function setDB($db) {
$this->db = $db;
}
public function auth() {
// Check if remeber me cookie is present
if (! isset($_COOKIE["auto"]) || empty($_COOKIE["auto"])) {
return false;
}
// Decode cookie value
if (! $cookie = @json_decode($_COOKIE["auto"], true)) {
return false;
}
// Check all parameters
if (! (isset($cookie['user']) || isset($cookie['token']) || isset($cookie['signature']))) {
return false;
}
$var = $cookie['user'] . $cookie['token'];
// Check Signature
if (! $this->verify($var, $cookie['signature'])) {
throw new Exception("Cokies has been tampared with");
}
// Check Database
$info = $this->db->get($cookie['user']);
if (! $info) {
return false; // User must have deleted accout
}
// Check User Data
if (! $info = json_decode($info, true)) {
throw new Exception("User Data corrupted");
}
// Verify Token
if ($info['token'] !== $cookie['token']) {
throw new Exception("System Hijacked or User use another browser");
}
/**
* Important
* To make sure the cookie is always change
* reset the Token information
*/
$this->remember($info['user']);
return $info;
}
public function remember($user) {
$cookie = [
"user" => $user,
"token" => $this->getRand(64),
"signature" => null
];
$cookie['signature'] = $this->hash($cookie['user'] . $cookie['token']);
$encoded = json_encode($cookie);
// Add User to database
$this->db->set($user, $encoded);
/**
* Set Cookies
* In production enviroment Use
* setcookie("auto", $encoded, time() + $expiration, "/~root/",
* "example.com", 1, 1);
*/
setcookie("auto", $encoded); // Sample
}
public function verify($data, $hash) {
$Rand = substr($hash, 0, 4);
return $this->hash($data, $Rand) === $hash;
}
private function hash($value, $Rand = null) {
$Rand = $Rand === null ? $this->getRand(4) : $Rand;
return $Rand . bin2hex(hash_hmac('sha256', $value . $Rand, $this->key, true));
}
private function getRand($length) {
switch (true) {
case function_exists("mcrypt_create_iv") :
$r = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
break;
case function_exists("openssl_random_pseudo_bytes") :
$r = openssl_random_pseudo_bytes($length);
break;
case is_readable('/dev/urandom') : // deceze
$r = file_get_contents('/dev/urandom', false, null, 0, $length);
break;
default :
$i = 0;
$r = "";
while($i ++ < $length) {
$r .= chr(mt_Rand(0, 255));
}
break;
}
return substr(bin2hex($r), 0, $length);
}
}
FirefoxとChromeでのテスト
利点
欠点
クイックフィックス
複数のCookieアプローチ
攻撃者がCookieを盗もうとしている場合、特定のWebサイトまたはドメインにのみフォーカスします。 example.com
しかし実際には、2つの異なるドメイン(example.com&fakeaddsite.com)そして、「広告Cookie」のように見せます
一部の人々は、2つの異なるCookieをどのように使用できるのかと疑問に思うかもしれません。可能です。example.com = localhost
とfakeaddsite.com = 192.168.1.120
を想像してください。 Cookieを検査すると、次のようになります
上の画像から
192.168.1.120
HTTP_REFERER
のみを受け入れますREMOTE_ADDR
からの接続のみを受け入れます利点
欠点
改善
ajax
を使用しました2つの非常に興味深い記事がありますが、「記憶」問題の完璧な解決策を探しているときに見つけました。
私はこの質問の1つの角度を尋ねました here 、そして答えはあなたが必要とするすべてのトークンベースのタイムアウトCookieリンクに導くでしょう。
基本的に、cookieにはuserIdを保存しません。ユーザーが古いログインセッションを取得するために使用するワンタイムトークン(巨大な文字列)を保存します。次に、本当に安全にするために、重い操作(パスワード自体の変更など)のパスワードを要求します。
Stefanが言及したアプローチを推奨します(つまり、 改善された永続的ログインCookieベストプラクティス のガイドラインに従います)。また、Cookieが HttpOnly cookies アクセスできない、潜在的に悪意のあるJavaScript。
古いスレッドですが、依然として有効な懸念事項です。セキュリティに関するいくつかの良い反応に気づき、「隠蔽によるセキュリティ」の使用を避けましたが、実際の技術的手法は私の目には不十分でした。メソッドを提供する前に言わなければならないこと:
以上のことから、システムで自動サインインを行うには2つの優れた方法があります。
まず、他の誰かにそれをすべて置く安価で簡単な方法。たとえば、Google +アカウントを使用したログインをサイトでサポートする場合、ユーザーが既にgoogleにサインインしている場合にログインする合理化されたgoogle +ボタンがあります(私はいつもこの質問に答えるためにここでそれを行いました)グーグルにサインイン)。信頼できるサポートされた認証システムで既にサインインしているユーザーに自動的にサインインさせたい場合は、チェックボックスをオンにして、ロードする前に対応する「サインイン」ボタンの背後にあるコードをクライアント側のスクリプトに実行させます、サーバーに、ユーザーに使用されるユーザー名、セッションID、オーセンティケーターを含む自動サインインテーブルに一意のIDを必ず保存させるようにしてください。これらのサインインメソッドはAJAXを使用するため、とにかく応答を待機しており、その応答は検証済み応答または拒否のいずれかです。検証済みの応答を受け取った場合は、通常どおり使用し、ログインしたユーザーの読み込みを通常どおり続行します。それ以外の場合、ログインは失敗しましたが、ユーザーに通知せずに、ログインしていない状態で続行すると、ユーザーは通知を受け取ります。これは、Cookieを盗んだ(または権限をエスカレートしようとして偽装した)攻撃者が、ユーザーがサイトに自動サインインすることを知ることを防ぐためです。
これは安価であり、GoogleやFacebookなどの場所で既にサインインしている可能性があることを、ユーザーに通知することなく検証しようとするため、一部のユーザーからは汚いと見なされることもあります。ただし、サイトへの自動サインインを求めていないユーザーには使用しないでください。この特定の方法は、Google +やFBなどの外部認証専用です。
外部認証システムを使用して、ユーザーが検証されたかどうかを舞台裏でサーバーに伝えるため、攻撃者は一意のID以外は取得できません。詳しく説明します。
何があっても、攻撃者が存在しないIDを使用しても、検証された応答を受信した場合を除き、すべての試行で試行は失敗するはずです。
このメソッドは、外部オーセンティケーターを使用してサイトにサインインするユーザーに対して、内部オーセンティケーターと組み合わせて使用できます。
=========
ユーザーを自動サインインできる独自のオーセンティケーターシステムの場合、次のようになります。
DBにはいくつかのテーブルがあります。
TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...
ユーザー名の長さは255文字までです。サーバープログラムでシステム内のユーザー名を32文字に制限していますが、外部認証システムでは@ domain.tldのユーザー名がそれよりも大きい場合があるため、互換性を最大限にするためにメールアドレスの最大長をサポートしています。
TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL
ログイン時にユーザー名がセッションデータに含まれ、プログラムがnullデータを許可しないため、このテーブルにはユーザーフィールドがありません。 session_idとsession_tokenは、ランダムmd5ハッシュ、sha1/128/256ハッシュ、ランダム文字列が追加されたハッシュ化された日時スタンプなどを使用して生成できますが、出力のエントロピーは許容できる限り高く維持する必要がありますブルートフォース攻撃が地面から降りるのを緩和します。セッションクラスで生成されたすべてのハッシュは、追加を試みる前に、セッションテーブルで一致をチェックする必要があります。
TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code
MACアドレスはその性質上、UNIQUEであると想定されているため、各エントリに一意の値があることは理にかなっています。一方、ホスト名は別々のネットワーク上で正当に複製される可能性があります。 「Home-PC」をコンピューター名の1つとして使用する人は何人いますか?ユーザー名はサーバーバックエンドによってセッションデータから取得されるため、操作することはできません。トークンに関しては、ページのセッショントークンを生成する同じ方法を使用して、ユーザーの自動サインイン用のCookieでトークンを生成する必要があります。最後に、ユーザーが資格情報を再検証する必要がある場合に備えて、日時コードが追加されます。ユーザーのログイン時にこの日時を更新して数日以内に保持するか、最後のログインが1か月程度しか保持しない場合でも、設計に応じて強制的に期限切れにします。
これにより、誰かが自動サインインを知っているユーザーのMACとホスト名を体系的にスプーフィングすることを防ぎます。NEVERユーザーに、パスワード、クリアテキストなどでCookieを保持させる。セッショントークンと同じように、各ページナビゲーションでトークンを再生成します。これにより、攻撃者が有効なトークンCookieを取得してログインに使用する可能性が大幅に低下します。一部の人々は、攻撃者が被害者からCookieを盗み、セッションリプレイ攻撃を行ってログインできると言うことを試みます。攻撃者が(可能であれば)Cookieを盗むことができれば、デバイス全体を確実に侵害していることになります。つまり、デバイスを使用してログインするだけで、Cookieを完全に盗むという目的は無効になります。サイトがHTTPS(パスワード、CC番号、または他のログインシステムを処理する場合)で実行されている限り、ブラウザー内でユーザーにできるすべての保護を提供します。
留意すべき点が1つあります。自動サインインを使用する場合、セッションデータは期限切れになりません。セッションを誤って続行する機能を失効させることができますが、システム間で検証を行うと、セッションデータがセッション間で継続すると予想される永続データである場合、セッションデータを再開する必要があります。永続的セッションデータと非永続的セッションデータの両方が必要な場合、PKとしてユーザー名を使用して永続セッションデータ用に別のテーブルを使用し、サーバーに通常のセッションデータと同様に取得させ、別の変数を使用します。
この方法でログインが完了しても、サーバーはセッションを検証する必要があります。これは、盗難または侵害されたシステムに対する期待をコーディングできる場所です。セッションデータへのログインのパターンやその他の予想される結果は、アクセスを得るためにシステムがハイジャックされたり、Cookieが偽造されたという結論につながることがよくあります。ここで、ISS Techがルールを設定して、アカウントのロックダウンまたは自動サインインシステムからのユーザーの自動削除をトリガーし、攻撃者が攻撃者の成功方法を判断するのに十分な時間、攻撃者を締め出します。それらを切断する方法。
最後の注意事項として、ユーザーが適切に検証してこれが発生したことを確認するまで、回復試行、パスワード変更、またはログイン失敗がしきい値を超えると、自動サインインが無効になることを確認してください。
私の答えでコードが提供されることを誰かが期待していた場合、私は謝罪します、それはここでは起こりません。私は、PHP、jQuery、およびAJAXを使用してサイトを実行し、サーバーとしてWindowsを使用することは決してないと言います。
おそらくあなただけが知っている秘密を使ってハッシュを生成し、それをユーザーに関連付けることができるようにDBに保存します。かなりうまくいくはずです。
ハッキングを行う必要があるのが暗号化されたものである場合、暗号化されたものをCookieに保存するという概念は理解できません。何か足りない場合はコメントしてください。
私はこのアプローチを「Remember Me」に取り入れることを考えています。問題があれば、コメントしてください。
「Remember Me」データを保存するテーブルを作成します-複数のデバイスからログインできるように、ユーザーテーブルとは別にします。
ログインに成功したら(Remember Meにチェックマークを付けて):
a)このマシンでUserIDとして使用される一意のランダム文字列を生成します:bigUserID
b)一意のランダム文字列を生成します:bigKey
c)Cookieを保存します:bigUserID:bigKey
d)「Remember Me」テーブルで、UserID、IP Address、bigUserID、bigKeyを含むレコードを追加します
ログインが必要なものにアクセスしようとする場合:
a)Cookieを確認し、一致するIPアドレスを持つbigUserIDとbigKeyを検索します
b)見つかった場合は、その人物にログインしますが、ユーザーテーブルに「ソフトログイン」フラグを設定します。これにより、危険な操作については、完全なログインを要求できます。
ログアウト時に、そのユーザーのすべての「Remember Me」レコードを期限切れとしてマークします。
私が見ることができる唯一の脆弱性は次のとおりです。
私の解決策はこのようなものです。 100%防弾というわけではありませんが、ほとんどの場合、あなたを救うと思います。
ユーザーが正常にログインすると、次の情報を含む文字列が作成されます。
$data = (SALT + ":" + hash(User Agent) + ":" + username
+ ":" + LoginTimestamp + ":"+ SALT)
$data
を暗号化し、タイプを HttpOnly に設定し、Cookieを設定します。
ユーザーがサイトに戻ってきたら、次の手順を実行します。
:
文字で展開します。ユーザーがサインアウトする場合、このCookieを削除します。ユーザーが再ログインする場合、新しいCookieを作成します。
私はすべての答えを読みましたが、それでも自分がすべきことを抽出するのが難しいと感じました。写真が1kワードに相当する場合、他の人がBarry Jaspanの Improved Persistent Login Cookie Best Practice に基づいて安全な永続ストレージを実装するのに役立つことを願っています
質問、フィードバック、または提案がある場合は、安全な永続的なログインを実装しようとしている初心者向けに図を更新しようとします。
「ログイン状態を保持」機能を実装すると、ユーザーにとって何を意味するかを正確に定義する必要があります。最も単純なケースでは、セッションのタイムアウトがはるかに長くなることを意味するために使用します:2時間ではなく2日(たとえば)。そのためには、おそらくデータベース内に独自のセッションストレージが必要になるため、セッションデータの有効期限を独自に設定できます。次に、ブラウザを閉じたときに期限が切れるのではなく、数日間(またはそれ以上)続くCookieを設定する必要があります。
「なぜ2日間なのか、なぜ2週間ではないのか」という質問を聞くことができます。これは、PHPでセッションを使用すると、有効期限が自動的にプッシュバックされるためです。これは、PHPでのセッションの有効期限が実際にはアイドルタイムアウトであるためです。
それでは、セッション自体に保存するタイムアウト値を2週間ほどで実装し、それを確認してセッションを強制的に無効にするコードを追加することをお勧めします。または、少なくともそれらをログアウトします。これは、ユーザーが定期的にログインするように求められることを意味します。 Yahoo!これを行います。