Usamos cookies para medir audiência e melhorar sua experiência. Você pode aceitar ou recusar a qualquer momento. Veja sobre o iMasters.
Antes de eu comentar o sistema de login eu preciso comentar as tabelas do mysql relacionadas e como fiz o sistema de cadastro.
**TABELA *users***
>
ID bigint(20) UNSIGNED auto_increment
user_md5_identity varchar(250)
user_login varchar(60)
user_pass varchar(64)
user_email varchar(100)
user_registered varchar(60)
user_status int(11)
display_name varchar(250)
main_language bigint(20)
Além dessa tabela existe outras que se relacionam com ela como userprofile, que contém as informações pessoais do usuário, mas não achei que seria relevante colocar a estrutura dela aqui também.
CADASTRO
Ao enviar o formulário eu salvo as informações de conta na tabela users e as pessoais em userprofile, nessa ordem. Protejo a consulta dando um anti-injection nas strings. A senha (user_pass) é criptografada com md5() e uso um hash por cima. $user_registered é a string que contém a data de inserção do usuário no formato do mysql. Depois que tenho as strings de cada campo do formulário eu crio a string "$user_md5_identity" que corresponde a coluna de mesmo nome na tabela users. O valor dessa coluna é um md5 da junção de user_login+user_email+user_registered+user_pass.
$user_md5_identity = md5($user_login.$user_email.$user_registered.$user_pass);
Vou explicar o por que dessa coluna na descrição de login abaixo.
LOGIN
Uma vez que você faz o login através do formulário e o login bate com a senha, eu salvo o session (ou o cookie se você marcar a opção de manter conectado), a única chave que salvo é $_SESSION["user"] e o valor é o user_md5_identity que peguei quando consultei se o login batia com a senha. Uma vez esse session no browser, eu verifico no php se existe algum usuario com o valor de $_SESSION["user"] na coluna user_md5_identity, se tiver eu registro o objeto $current_user com as informções da tabela users e userprofile. Eu criei esse user_md5_identity, porque não acredito na segurança dos cookies, e creio que dessa forma fica mais seguro e não salvo quaisquer informação do usuário seja login ou senha.
CONCLUSÃO
Não creio que as informações que passei seja o suficiente pra dizer se o meu sistema de login está seguro ou não porque há muitos outros fatores, e não sei até que ponto o uso dessa única sessão com uma chave única em md5 se faz relevante quanto a segurança. Gostaria de saber se isto melhora a segurança e se pudessem me perguntar quanto a outras informações sobre a segurança desse sistema para eu responder seria de muita valia pra mim, sendo que abri esse tópico porque é a primeira vez que utilizo um sistema de login feito por mim.
É que eu criei várias classes para facilitar processo de inserção, validação e etc... O código abaixo é do formulário de login:
Explicações de algumas variáveis:
$form = Eu fiz essa classe para facilitar validação de formulário, pegar valores e etc...
$nonce = Também criei essa classe, mas com intuito de validar um usuário antes de enviar o formulário e depois de enviá-lo da pra enviar o valor de nonce tanto por POST como por GET.
O código abaixo vem antes do cabeçalho da página.
<?php
global $captcha, $form, $nonce, $current_user, $user_ID;
$form->print_error = '<div class="notbox error"><p>%s</p></div>';
if($_POST) :
if($nonce->verify($_POST["nonce"])) {
//ACCEPTED ARGS FROM POST
$form->accepted = array("login", "password", "captcha", "nonce", "permanent");
//RENAME ARGS ON DATA RETURN
//VALIDATION SCRIPT
$form->filter("errors", "valitation");
function valitation($errors, $data, $validation) {
global $captcha, $nonce, $user;
/*
if(!$errors["captcha"]) {
if(trim($_POST["captcha-{$captcha->current_sid}"]) == "")
$errors["captcha"] = __("Você precisa digitar o código de segurança!", THEME);
elseif(!$captcha->sid_exist($captcha->current_sid))
$errors["captcha"] = __("Não foi possível registrar o código de segurança. Digite-o novamente.", THEME);
elseif($captcha->expired($captcha->current_sid))
$errors["captcha"] = __("O código de segurança que você digitou expirou. Digite-o novamente.", THEME);
elseif(!$captcha->check($_POST["captcha-{$captcha->current_sid}"]))
$errors["captcha"] = __("O código de segurança que você digitou estava errado, por favor, digite-o novamente!", THEME);
}
*/
if(!$errors["login"]) {
if(empty($data["login"]))
$errors["login"] = __("É necessário que você digite o seu login.", THEME);
else {
if(strpos($data["login"], "@meudominio.com") !== false) {
$data["login"] = trim(str_replace("@meudominio.com", "", $data["login"]));
}
if(strpos($data["login"], "@") !== false) {
if(!$user = get_userdatabyemail($data["login"]))
$errors["login"] = __("O endereço de e-mail informado está incorreto.", THEME);
} elseif(!$user = get_userdatabylogin(sanitize_user($data["login"])))
$errors["login"] = __("O nome de usuário informado está incorreto.", THEME);
}
}
if(!$errors["password"] && !$errors["login"]) {
if(empty($data["password"]))
$errors["password"] = __("É necessário que você digite uma senha!", THEME);
elseif(!check_password($data["password"], $user->user_pass, $user->ID)) {
$errors["password"] = __("A senha está incorreta!", THEME);
}
}
if(!$nonce->verify($data["nonce"])) {
$errors["nonce"] = __("Algumas informações com relação a sua identidade foram trocados durante o processo do cadastro. Abaixo algumas informações do porque aconteceu isso:<strong style='margin-top: 5px; display: block'>1 - Você limpou seus cookies durante o processo ou seu navegador não é capaz de armazenar cookies.</strong><strong style='margin-top: 5px; display: block'>2 - Você atualizou seu IP.</strong><strong style='margin-top: 5px; display: block'>3 - O método de envio do formulário é diferente do método registrado antes do envio.</strong><strong style='margin-top: 5px; display: block'>4 - A página de onde o formulário foi enviado não corresponde a um de nossos dominios.</strong>", THEME);
}
return $errors;
}
//EXECUTE DATA
$form->data($_POST);
//AFTER EVERYTHING WAS COMPLETE
if($form->success) {
if(strpos($form->data["login"], "@meudominio.com") !== false)
$form->data["login"] = trim(str_replace("@meudomionio.com", "", $form->data["login"]));
if(strpos($form->data["login"], "@") !== false)
$user = get_userdatabyemail($form->data["login"]);
else
$user = get_userdatabylogin(sanitize_user($form->data["login"]));
deactive_tokenbyuser($user->ID, -3);
if($token = create_token($user->ID, (time()+60*60*24*30))) {
$_SESSION["user"] = $user->user_md5_identity;
redirect(get_siteinfo("url"));
exit;
} else {
$form->success = false;
$form->errors["cookies"] = __("Erro ao salvar cookies.", THEME);
}
}
//NEVER GET THE PASSWORD FROM POST
} else {
$form->errors["nonce"] = __("Algumas informações com relação a sua identidade foram trocados durante o processo do cadastro. Abaixo algumas informações do porque aconteceu isso:<strong style='margin-top: 5px; display: block'>1 - Você clicou em voltar do seu navegador e reenviou um formulário que já fora enviado.</strong><strong style='margin-top: 5px; display: block'>2 - Você limpou seus cookies durante o processo ou seu navegador não é capaz de armazenar cookies.</strong><strong style='margin-top: 5px; display: block'>3 - Você atualizou seu IP.</strong><strong style='margin-top: 5px; display: block'>4 - O método de envio do formulário é diferente do método registrado antes do envio.</strong><strong style='margin-top: 5px; display: block'>5 - A página de onde o formulário foi enviado não corresponde a um de nossos dominios.</strong>", THEME);
}
if(!$form->success) {
if(!$form->errors["password"]) $form->errors["password"] = __("Digite a sua senha novamente!", THEME);
}
endif;
?>Pelo que eu vi,dá pra usar Cross Site Session Forgery ou como alguns chamam Session Hijacking.
Não posso afirmar que não tenham outras falhas,já que não o avaliei direito.
Seu script tá estranho...sugiro que se puder arranje um mais atualizado.
Qual script? Sendo que todo o código que tá ai dentro foi feito por mim, então não teria como eu pegar um mais atualizado.
Estou dando uma olhada nessas informações que você me passou sobre Cross Site Session Forgery e Session Hijacking.
Eu achei interessante o seu sistema de login, agora dizer se é seguro, é outra história. Vou dizer por mim.
Primeiramente, eu avalio, se preciso realmente de um sistema de login tão seguro antes de começar. Sabemos
que nenhum sistema é 100% seguro, então, eu evito trabalho extra quando não é necessário, e crio apenas
um hash como senha e faço a verificação. Em .net, há como você usar o login live, ou o login do windows, em
PHP não sei se é possível, então, geralmente, temos que fazer o trabalho na unha mesmo, por isso começo
fazendo essa analise crítica.
Porém, se estou desenvolvendo um aplicativo finaneiro, ou algo que justifique minha preocupação com a segurança
do login, começo com o protocolo de segurança, já que é possível monitorar o tráfego da rede. Com uma
criptografia fica bem mais difícil né? Depois disso, sei perfeitamente, que se um atacante obtem acesso ao banco de
dados, então de nada adianta um bilhão de criptografias na senha do usuário comum. Por isso, crio um usuário no
banco de acesso limitado ao necessário. Logo após, desenvolvo as tabelas de usuario, costumo criar um hash SHA1
da senha, em vez de gravá-la como o usuário digita. Não sei se é de grande preocupação, ataques de sessão, quando
se usa cookies, o que sempre faço. Se o usuário não habilita cookies, não está apto a logar no meu sistema.
procuro renovar a cada requisição o id de sessão, e evito gravar dados de grande importância em cookies, ou sessão.
As sessões são gravadas no servidor sem qualquer criptografia, porém, ter acesso à essas sessões, é quase impossível
creio eu, mas eu prefiro não arriscar.
Bom , esta é a minha opnião.
Concordo com o que você disse, porém esse sistema precisa ser o mais seguro possível, embora não seja um aplicativo finaneiro, mas precisa de uma segurança reforçada. Então deixa eu explicar melhor sobre o sistema.
>
1. SESSION Quando você disse "porém, ter acesso à essas sessões, é quase impossível
creio eu, mas eu prefiro não arriscar".Também penso assim, por isso a idéia de criar uma chave única do usuário com a junça de login+email+data de registro+senha e criptografar com md5. Daí só criaria a sessão $_SESSION["user"] = "c85f59f21636a39f7985bb09aa07d318"; Ou seja, a minha idéia é que se houver uma manipulação de session é praticamente impossível você adivinhar o que ta dentro dessa criptografia e se souber de nada adiantar porque para criar a criptografia md5 correta você precisa ter o login, email, data de registro e a senha criptografada do usuário. Então creio que é praticamente 0% de probabilidade de conseguir invadir por session.
>
2. NONCE Eu criei essa classe nonce para tentar fortificar a segurança dos formulários, funciona da seguinte maneira.
O código da página abaixo corresponde ao código html que fica abaixo do código que postei logo acima. Ou seja, o formulário de login envia para a mesma página.
<?php if($user_ID) : // SETIVER LOGADO ?>
<p>Olá, <?php echo $current_user->first_name; ?>! <a href="<?php siteinfo("url"); ?>?logout&nonce=<?php echo $nonce->generate("logout", "ip&referer&user"); ?>">logout</a>.</p>
<?php else : // SENAO TIVER LOGADO ?>
<form action="<?php siteinfo("url"); ?>" method="post">
<fieldset>
<legend>Entrar</legend>
<p class="">
<label for="login-passaporte">login ou e-mail</label>
<input name="login" id="login-passaporte" value="<?php $form->value("login"); ?>" type="text" />
</p>
<?php $form->print_error("login"); ?>
<p class="">
<label for="senha-passaporte">senha</label>
<input name="password" id="senha-passaporte" class="txt" type="password" />
</p>
<?php $form->print_error("password"); ?>
<input value="<?php siteinfo("url"); ?>" name="redirect" type="hidden" />
<input value="<?php echo $nonce->generate("login", "ip&referer"); ?>" name="nonce" type="hidden" />
<input name="permanent" id="permanent" value="true" type="checkbox">
<label for="permanent"><strong>Me manter conectado</strong> <span>Não assinalar se estiver utilizando um computador compartilhado</span></label>
<input value="acessar" type="submit">
<?php $form->print_error("cookies"); ?>
<?php $form->print_error("nonce"); ?>
</fieldset>
</form>
<?php endif; ?>
Ou seja, esse
<input value="<?php echo $nonce->generate("login", "ip&referer"); ?>" name="nonce" type="hidden" />geraria um código que seria confirmado na página para qual o formulário foi enviado, ou seja, o [código que postei logo acima](http://forum.imasters.com.br/index.php?showtopic=340823&view=findpost&p=1271564) que esta na mesma página do formulário irá receber e validar o login.
<?php echo $nonce->generate("login", "ip&referer"); ?>O primeiro atributo dentro de generate é "login", é o nome do nonce que gerei, o segundo valor é que atributos esse nonce está verificando. Então na próxima página ele irá verificar se o IP é o igual ao IP que enviou o formulário e se a referência, ou seja, a url da página atual pra qual o código nonce foi gerado deve ser igual a página anterior para o qual o formulário foi enviado.Ou seja, um session é criado. Basicamente seria algo assim, mas dentro da classe o negócio é bem mais complexo que isso, mas resumidamente funciona desse jeito.
$code .= get_the_ip();
$url = strtolower(preg_replace("/([^\/]+)\/(.+)/i", "$1", $_SERVER["SERVER_PROTOCOL"]))."://";
$url .= $_SERVER["SERVER_NAME"];
$url .= $_SERVER["REQUEST_URI"];
$code .= $url;
$code = md5($code);
$_SESSION["nonce"]["login"] = serialize(array($code => "ip&referer"));
return $code;O $code retornado seria o echo de
<?php echo $nonce->generate("login", "ip&referer"); ?>e no [código que postei logo acima](http://forum.imasters.com.br/index.php?showtopic=340823&view=findpost&p=1271564) eu verifico esse valor
$nonce->verify($_POST["nonce"])O que a verificação faz é procurar pelo código informado dentro de $_SESSION["nonce"], se achar ele traz os valores "ip&referer". Daí eu sei que esse código foi criado com base no ip e na referencia.Então a verificação seria algo mais ou menos assim.
$code; //é o atributo passado na função, ou seja, $_POST["nonce"].
$types = array("ip", "referer"); // são as chaves utilizadas para criar o $code acima
$values = array();
$vcode = "";
foreach($types as $type) if(in_array($type, $this->types)) {
$value = $this->filters("nonce_verify_$type"); // eu criei isso para chamar as funções salvas para retornar o valor da chave ip por exemplo "nonce_verify_ip"
if($value) {
$vcode .= $value;
$values[$type] = $value;
}
$values[$type] = $value;
}
$this->remove($code); //REMOVE O $code da SESSAO
return ($code == md5($vcode)); //Daí ele verifica se o código gerado (md5($vcode)) é iguál ao código passado.
para adicionar esses filtros em $this->filters("nonce_verify_$type"); você faria assim:
$this->types = array("ip", "referer", "time", "user"); // Além de adicionar os filtros abaixo eu preciso salvar se os tipos.
//Aqui é na hora de gerar
$this->add("nonce_generate_ip", array(&$this, "ip_generate"));
$this->add("nonce_generate_referer", array(&$this, "referer_generate"));
$this->add("nonce_generate_time", array(&$this, "time_generate"));
$this->add("nonce_generate_user", array(&$this, "user_generate"));
// AQUI é na hora de verificar
$this->add("nonce_verify_ip", array(&$this, "ip_verify"));
$this->add("nonce_verify_referer", array(&$this, "referer_verify"));
$this->add("nonce_verify_time", array(&$this, "time_verify"));
$this->add("nonce_verify_user", array(&$this, "user_verify"));
function ip_generate() {
return getenv("REMOTE_ADDR");
}
function ip_verify() {
return getenv("REMOTE_ADDR");
}
function referer_generate() {
$this->url = strtolower(preg_replace("/([^\/]+)\/(.+)/i", "$1", $_SERVER["SERVER_PROTOCOL"]))."://";
$this->url .= $_SERVER["SERVER_NAME"];
$this->url .= $_SERVER["REQUEST_URI"];
return $this->url;
}
function referer_verify() {
$referer = "";
if($_SERVER["REFERER"]) $referer = $_SERVER["REFERER"];
elseif($_SERVER['HTTP_REFERER']) $referer = $_SERVER['HTTP_REFERER'];
return $referer;
}Ps.: Acho que esse nonce que eu fiz seria uma idéia semelhante a esse "Session Hijacking" que eibon citou.
3. CAPTCHA Quando você tenta fazer login mais de 4 vezes sem sucesso antão um captcha aparece pra você digitar os caracteres
>
4. TOKEN DE USUÁRIO Esse ainda não tá 100%, eu estava tentando criar uma espécie de registro de login, a tabela se chama usertoken e a estrutura é mais ou menos assim:
>
token_ID bigint(20)
token_key varchar(250)
token_status int(11)
token_type varchar(250)
referer_url varchar(250)
referer_title text
page_url varchar(250)
page_title text
created_date datetime
updated_date datetime
expire_date datetime
deleted_date datetime
user_agent varchar(250)
user_ip varchar(40)
user_status int(11)
user_id bigint(20)
Funcionaria como uma espécie de cookie dentro do mysql, além de um rastreador do usuário e também poder verificar se ele tá online.token_status menores que 1 são tokens desativados ou seja:
0 = expirado
-1 = logout
-2 = informacoes trocadas (como ip e user agent)
-3 = efetuou login por cima de login
Eu usaria esse token não só como segurança, mas também para verificar se um usuário está online através de outro site.
No código que postei logo acima tenho referencia desse token nessas funcões.
deactive_tokenbyuser($user->ID, -3);
if($token = create_token($user->ID, (time()+60*60*24*30))) {
$_SESSION["user"] = $user->user_md5_identity;
redirect(get_siteinfo("url"));
exit;
}
A principio a segurança se baseia nisso SESSION, NONCE, CAPTCHA e TOKEN.
A coluna users é super desnecessária...e é impossivel dizer se é seguro ou não só por uma tabela da sua database -.-.
Passe-nos mais dados (code).