Mat*_*hew 245 php security session remember-me
我的Web应用程序使用会话在用户登录后存储有关用户的信息,并在应用程序中从一个页面移动到另一个页面时维护该信息.在这个特定的应用程序,我存储user_id
,first_name
和last_name
人的.
我想在登录时提供"Keep Me Logged In"选项,这将在用户的计算机上放置一个cookie两周,这将在他们返回应用程序时以相同的细节重新启动他们的会话.
这样做的最佳方法是什么?我不想将它们存储user_id
在cookie中,因为看起来这样可以让一个用户轻松尝试伪造另一个用户的身份.
irc*_*ell 721
好吧,让我直言不讳地说:如果你为了这个目的将用户数据或从用户数据派生的任何内容放入cookie中,那么你做错了.
那里.我说了.现在我们可以转到实际的答案.
你问,散列用户数据有什么问题?嗯,它归结为曝光表面和通过默默无闻的安全性.
想象一下,你是一个攻击者.您会在会话中看到为记住我设置的加密cookie.这是32个字符宽.啧啧.那可能是MD5 ......
让我们想象一下,他们知道你使用的算法.例如:
md5(salt+username+ip+salt)
Run Code Online (Sandbox Code Playgroud)
现在,攻击者需要做的就是蛮力"盐"(这不是真正的盐,但稍后会更多),现在他可以用他的IP地址的任何用户名生成他想要的所有假令牌!但强迫盐很难,对吧?绝对.但现代GPU非常擅长.除非你使用足够的随机性(使它足够大),否则它会快速下降,并且随之而来的是城堡的钥匙.
简而言之,唯一能保护你的是盐,它并不像你想象的那样真正保护你.
可是等等!
所有这一切都预示着攻击者知道算法!如果这是秘密和混乱,那么你是安全的,对吧?错了.这种思路有一个名称:安全通过晦涩,永远不应该依赖.
更好的方式
更好的方法是永远不要让用户的信息离开服务器,除了id.
当用户登录时,生成一个大的(128到256位)随机令牌.将其添加到将令牌映射到用户ID的数据库表中,然后将其发送到cookie中的客户端.
如果攻击者猜到另一个用户的随机令牌怎么办?
好吧,我们在这里做一些数学.我们正在生成一个128位随机令牌.这意味着有:
possibilities = 2^128
possibilities = 3.4 * 10^38
Run Code Online (Sandbox Code Playgroud)
现在,为了表明这个数字是多么荒谬,让我们想象一下互联网上的每台服务器(比如今天的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
Run Code Online (Sandbox Code Playgroud)
所以每秒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
Run Code Online (Sandbox Code Playgroud)
所以6.8性别秒......
让我们尝试将其归结为更友好的数字.
215,626,585,489,599 years
Run Code Online (Sandbox Code Playgroud)
甚至更好:
47917 times the age of the universe
Run Code Online (Sandbox Code Playgroud)
是的,这是宇宙年龄的47917倍......
基本上,它不会被破解.
总结一下:
我推荐的更好的方法是将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);
}
Run Code Online (Sandbox Code Playgroud)
然后,验证:
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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
注意:请勿使用令牌或用户和令牌的组合来查找数据库中的记录.始终确保根据用户获取记录,并使用计时安全比较功能来比较之后获取的令牌.更多关于定时攻击.
现在,非常重要的SECRET_KEY
是加密秘密(由类似/dev/urandom
和/或从高熵输入得到的东西生成).此外,GenerateRandomToken()
需要一个强大的随机源(mt_rand()
是远远不够强,使用图书馆,如RandomLib或random_compat,或mcrypt_create_iv()
用DEV_URANDOM
)...
这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;
}
Run Code Online (Sandbox Code Playgroud)
Pim*_*ger 93
安全注意事项:将cookie从确定性数据的MD5哈希中删除是一个坏主意; 最好使用从CSPRNG派生的随机令牌.有关更安全的方法,请参阅ircmaxell对此问题的回答.
通常我做这样的事情:
当然,您可以使用不同的cookie名称等.您也可以稍微更改cookie的内容,只需确保不要轻易创建.例如,您还可以在创建用户时创建user_salt,并将其放入cookie中.
你也可以使用sha1而不是md5(或几乎任何算法)
Bab*_*aba 76
介绍
您的标题"让我登录" - 最好的方法让我很难知道从哪里开始,因为如果您正在寻找最佳方法,那么您将不得不考虑以下事项:
饼干
Cookie易受攻击,在常见的浏览器cookie-theft漏洞和跨站点脚本攻击之间,我们必须接受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());
}
Run Code Online (Sandbox Code Playgroud)
使用的类
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);
}
}
Run Code Online (Sandbox Code Playgroud)
在Firefox和Chrome中测试
优点
坏处
快速解决
多个Cookie方法
当攻击者即将窃取cookie时,只关注特定网站或域名,例如.example.com
但实际上,您可以对来自2个不同域(example.com和fakeaddsite.com)的用户进行身份验证,并使其看起来像"广告Cookie"
有些人可能想知道你如何使用2种不同的饼干?那可能,想象example.com = localhost
和fakeaddsite.com = 192.168.1.120
.如果您检查cookie,它将看起来像这样
从上面的图像
192.168.1.120
HTTP_REFERER
REMOTE_ADDR
优点
坏处
起色
ajax
小智 6
旧线程,但仍然是一个有效的问题。我注意到一些关于安全性的良好反应,并避免使用“通过默默无闻的安全性”,但给出的实际技术方法在我看来还不够。在我贡献我的方法之前我必须说的事情:
话虽如此,有两种很好的方法可以在您的系统上进行自动登录。
首先,廉价、简单的方法把一切都放在别人身上。如果你让你的网站支持登录,比如你的 google+ 帐户,你可能有一个简化的 google+ 按钮,如果用户已经登录谷歌,它会登录用户(我在这里回答这个问题,因为我总是登录谷歌)。如果您希望用户在用户已使用受信任且受支持的身份验证器登录时自动登录,并选中该框以执行此操作,请让您的客户端脚本在加载之前执行相应的“登录方式”按钮后面的代码,只需确保服务器在具有用户名、会话 ID 和用于用户的身份验证器的自动登录表中存储唯一 ID。由于这些登录方法使用 AJAX,因此您无论如何都在等待响应,该响应要么是经过验证的响应,要么是拒绝。如果您收到经过验证的响应,请照常使用它,然后继续照常加载登录用户。否则,登录失败,但不要告诉用户,只是继续作为未登录,他们会注意到。这是为了防止窃取 cookie(或伪造 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
...
Run Code Online (Sandbox Code Playgroud)
请注意,用户名的长度可以为 255 个字符。我的服务器程序将系统中的用户名限制为 32 个字符,但外部身份验证器的用户名可能比其 @domain.tld 大,因此我只支持电子邮件地址的最大长度以实现最大兼容性。
TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL
Run Code Online (Sandbox Code Playgroud)
注意这个表中没有用户字段,因为用户名在登录时是在会话数据中的,程序不允许空数据。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
Run Code Online (Sandbox Code Playgroud)
MAC 地址本质上应该是唯一的,因此每个条目都有一个唯一的值是有道理的。另一方面,主机名可以合法地在不同的网络上复制。有多少人使用“Home-PC”作为他们的计算机名称之一?用户名是由服务器后端从会话数据中获取的,因此无法对其进行操作。至于令牌,应该使用为页面生成会话令牌的相同方法在 cookie 中为用户自动登录生成令牌。最后,当用户需要重新验证其凭据时,会添加日期时间代码。要么在用户登录时更新此日期时间,将其保留在几天内,要么强制它过期,无论上次登录如何,只保留一个月左右,无论您的设计如何。
这可以防止有人系统地欺骗他们知道自动登录的用户的 MAC 和主机名。永远不要让用户使用他们的密码、明文或其他方式保存 cookie。在每个页面导航上重新生成令牌,就像会话令牌一样。这大大降低了攻击者获取有效令牌 cookie 并使用它登录的可能性。有些人会试图说攻击者可以从受害者那里窃取 cookie 并进行会话重放攻击以登录。如果攻击者可以窃取 cookie(这是可能的),他们肯定会破坏整个设备,这意味着他们无论如何都可以使用设备登录,这完全违背了窃取 cookie 的目的。只要您的站点通过 HTTPS 运行(在处理密码、CC 号码或其他登录系统时应该这样做),您就已经为用户提供了在浏览器中可以提供的所有保护。
要记住的一件事:如果您使用自动登录,会话数据不应过期。您可以错误地使继续会话的能力失效,但如果会话数据是预期在会话之间继续的持久数据,则在系统中验证应该恢复会话数据。如果您需要持久和非持久会话数据,请使用另一个表来保存以用户名作为 PK 的持久会话数据,并让服务器像正常会话数据一样检索它,只需使用另一个变量。
以这种方式登录后,服务器仍应验证会话。在这里,您可以为被盗或受感染的系统编写预期代码;登录会话数据的模式和其他预期结果通常会导致系统被劫持或伪造 cookie 以获取访问权限的结论。在这里,您的 ISS 技术人员可以设置规则,触发帐户锁定或从自动登录系统中自动删除用户,将攻击者拒之门外的时间足够长,以便用户确定攻击者如何成功以及如何将其切断。
最后,请确保任何超过阈值的恢复尝试、密码更改或登录失败都会导致自动登录被禁用,直到用户正确验证并确认发生了这种情况。
如果有人希望在我的回答中给出代码,我深表歉意,这不会发生在这里。我会说我使用 PHP、jQuery 和 AJAX 来运行我的网站,而且我从不使用 Windows 作为服务器......永远。
我建议Stefan提到的方法(即遵循改进的持久登录Cookie最佳实践中的指导原则),并建议您确保您的cookie是HttpOnly cookie,因此它们无法访问,可能是恶意的JavaScript.
归档时间: |
|
查看次数: |
138931 次 |
最近记录: |