Questa area di amministrazione è abbastanza sicura? V2

5

Questa è una risposta alla mia domanda precedente, Questa area di amministrazione è sufficientemente sicura? .

Ci sono state alcune risposte molto utili lì, per le quali sono molto grato. Così sono tornato al tavolo da disegno ed ecco la mia nuova domanda, perché sono nuovo per il lato sicurezza delle cose e non sono sicuro di aver capito o coperto tutti i buchi del ciclo correttamente .

I problemi che sto cercando di proteggere dal sistema sono:

Note

  • Le password sono memorizzate con questa implementazione di bcrypt
  • L' intero sito utilizzerà il protocollo https: //
  • Flags session.cookie_secure e session.cookie_httponly essere impostato
  • L' oggetto PDO di PHP viene utilizzato in tutte le query SQL, tutti gli input sono parametrizzati
  • Qualsiasi variabile di input dell'utente verrà disinfettata
  • Ogni pagina inizierà con un'intestazione http x-frame-options: deny
  • Ogni pagina con restrizioni inizia con uno script di autorizzazione (vedere la figura 1)
  • Tutti i moduli saranno autorizzati solo se la variabile impostata $_SESSION["auth_key"] corrisponde alla chiave inviata con il modulo (vedi figura 1)

Fig.1: Autorizzazione & Script di accesso


Sperochequestodiagrammasiaperlopiùautoesplicativo.Tuttavia,cercheròdichiarirealcunefunzioni.

  • controllatoken:untokengeneratounavoltacheunutenteaccede,memorizzatoin$_SESSION['auth_token']einviatoconqualsiasiformachel'utenteinvia.
  • verificasessione:sel'utentehauntokenvalido,estraeirelatividettaglidaldatabaseutilizzandoPHPSESSID(ilnomeènecessarioperdareilbenvenutoall'utente).Sel'IDdisessionenonvienetrovatooiltokennonèvalido,lalorosessioneèscadutaedevonoeffettuarenuovamenteillogin.
  • Regen.sessione:unavoltachel'utentehaeffettuatol'accessoconsuccesso,dagliunnuovoIDdisessione(session_regenerate_id())eaggiornaildatabaseconilnuovoIDdisessione.Imposta$_SESSION['auth_token']=md5(mt_rand(1,10000).$username);.Ilnomeutenteèunico,quindiquestodovrebbegenerareuntokendiautenticazioneunivocoinmodochepossanoinviareiproprimoduli.Nonsonosicurodiaverloimplementatocorrettamente,perfavorevedi esempio qui
  • verifica login: controlla semplicemente che la password briptata corrisponda all'ingresso briptato e che i nomi utente corrispondano.
  • tentativi falliti: questo lato viene aggiunto per rallentare gli attacchi di forza bruta; prima viene aggiunto un captcha, quindi l'IP viene temporaneamente bloccato e l'utente / titolare dell'account riceve un'e-mail di notifica.
  • Aggiungerò ulteriori dettagli se necessario / correggi questi dettagli in caso di errori

Implementazione (finora):

Pagina con restrizioni: inizia con il requisito di authenticate.php

<?php require_once "authenticate.php"; ?>

<p>Welcome <?php echo $_SESSION["guest"]; ?>!
<a href="index.php?action=logout">Logout</a></p>

<p><a href="index.php?token=<?php echo $_SESSION["auth_token"] ?>">Try again.</a></p>

Authenticate.php: reindirizza a login.php se non autenticato
Nota: ho lasciato fuori la crittografia (durante il test) perché bcrypt non è disponibile nella mia versione di PHP ( 5.2.17) quindi sto cercando SHA256 o SHA512.

<?php
    session_start();
    // load database abstraction layer
    require_once "../../dal.php";
    unset($_SESSION["login_errors"]);

function loginError($str) {
    if (isset($_SESSION["login_errors"]))
        $_SESSION["login_errors"] = "" . $_SESSION["login_errors"] . "; " . $str;
    else
        $_SESSION["login_errors"] = $str;
}
function hasValidToken() {
    return (isset($_SESSION["auth_token"]) && isset($_GET["token"])
    && $_SESSION["auth_token"] == $_GET["token"]);
}
function hasValidSession() {
    return (session_id()? hasOpenSession() : false);
}
function hasOpenSession() {
    $sql = "SELECT * FROM tbl_store_admin WHERE php_sesskey=?;";
    $data = array(session_id());
    return (dbRowsCount($sql, $data) == 1);
}
function updateUserSession() {
    $sql = "UPDATE tbl_store_admin SET php_sesskey=? WHERE username=?";
    $data = array(session_id(), $_SESSION["uid"]);
    dbQuery($sql, $data);
    return (dbRowsAffected() == 1);
}
function hasLoggedIn() {
    if (isset($_POST["uid"]) && isset($_POST["key"])) {
        $uid = htmlspecialchars($_POST["uid"]);
        $key = htmlspecialchars($_POST["key"]);
        return (getUser($uid, $key));
    } else {
        return false;
    }
}
function wrongCredentials() {
    if (isset($_SESSION["login_attempts"])) {
        $_SESSION["login_attempts"] = $_SESSION["login_attempts"]+1;
    } else {
        $_SESSION["login_attempts"] = 1;
    }
    logout();
    loginError("Bad Credentials");
    return false;
}
function getUser($uid, $key) {
    $sql = "SELECT * FROM tbl_store_admin WHERE username=? AND keycode=? LIMIT 1;";
    $data = array($uid, $key);
    $rows = dbRowsCount($sql, $data);
    if ($rows == 1) {
        dbQuery($sql, $data);
        $user = dbFetch();
        // store data in session
        $_SESSION["uid"] = $user["username"];
        $_SESSION["guest"] = $user["nickname"];
        return true;
    } else {
        return wrongCredentials();
    }
}
function logout() {
    if (isset($_SESSION["auth_token"])) {
        unset($_SESSION["auth_token"]);
    }
    if (isset($_SESSION["uid"])) {
        $sql = "UPDATE tbl_store_admin SET php_sesskey=NULL WHERE username=?;";
        $data = array($_SESSION["uid"]);
        dbQuery($sql, $data);
        unset($_SESSION["uid"]);
    }
    if (isset($_SESSION["guest"])) {
        unset($_SESSION["guest"]);
    }
}
function hasAuthenticated() {
    if (hasValidToken()) {
        if (hasValidSession()) {
            return true;
        } else {
            logout();
            return false;
        }
    } else {
        logout();
        return hasLoggedIn();
    }
}
function regenerateSession() {
    session_regenerate_id();
    updateUserSession();
    $token = "" . mt_rand(1,10000) . $_SESSION["uid"];
    $_SESSION["auth_token"] = md5($token);
}
if (isset($_GET["action"]) && $_GET["action"]=="logout") {
    logout();
}
if (!hasAuthenticated()) {
    header('Location: login.php');
} else {
    regenerateSession();
}

?>

Login.php: un semplice modulo di accesso

<?php
session_start();
function getRequestURL() {
    if (isset($_SESSION['HTTP_REFERER'])) {
        return $_SESSION['HTTP_REFERER'];
    }
    return "index.php?token=".$_SESSION["auth_token"];
}
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
<head>
<meta http-equiv="X-Frame-Options" content="deny">

<title>Admin Login</title>

<style type="text/css">
.errorbox {
 position:absolute;
 top:0px;
 left:0px;
 width:100%;
 height:100px;
 background-color:#FFAAAA;
 border:1px solid #FF0000;
}
.loginbox {
 width:600px;
 height:300px;
 border:5px solid #000000;
 margin:100px auto;

border-radius:10px;
 -moz-border-radius:10px;
 -webkit-border-radius:10px;
 background-color:#CCCCCC;

box-shadow:4px 4px 2px #999999;
 -moz-box-shadow:4px 4px 2px #999999;
 -webkit-box-shadow:4px 4px 2px #999999;
}
.formbox {
 width:400px;
 height:200px;
 margin:0px auto;
}
.label {
 width:120px;
 height:40px;
 text-align:right;
 line-height:40px;
 float:left;
 font-weight:bold;
}
.textinput {
 height:40px;
 width:260px;
 font-size:30px;
 line-height:40px;
}
.submitbtn {
 height:50px;
 width:100px;
 font-size:30px;
 line-height:40px;
}
</style>
</head>

<body style="font-family: Arial;font-size:20px;">

<?php
if (isset($_SESSION["login_errors"])) {
?>
    <div class="errorbox">
        Error: <?php echo ($_SESSION["login_errors"]); ?>
    </div>
<?php
}
?>

<div class="loginbox">
<h1 align="center">Admin Area</h1>

<div class="formbox">
<form action="<?php echo getRequestURL(); ?>" method="POST">
<p><big>
<div class="label">Name:</div>&nbsp;
<input class="textinput" type="text" name="uid"/><br>
<div class="label">Password:</div>&nbsp;
<input class="textinput" type="password" name="key"/><br>
</big></p>
<p align="right"><input class="submitbtn" type="submit" value="Log In"/></p>
</form>
</div>

</div>

</body>
</html>
    
posta Community 26.04.2012 - 23:30
fonte

1 risposta

4

Nel complesso. Questo è molto meglio. Ma ho ancora alcuni commenti -

Clickjacking. Il tuo diagramma sulla prevenzione del clickjacking è terribilmente vago. Presumo che tu stia facendo framebusting sul lato client (usando Javascript che gira sul client). Sappiate che questo è estremamente soggetto a errori e la maggior parte delle persone che scrivono il proprio codice framebusting finisce con qualcosa che è sottilmente rotto. Un documento accademico ha esaminato i 500 siti più utilizzati (nel 2010) e ha rilevato che ogni singolo sito che utilizzava framebusting, lo implementava in un modo non sicuro . Gli attacchi erano sottili e non ovvi, quindi gli sviluppatori probabilmente pensavano che il loro codice fosse valido - quando in realtà era imperfetto.

Ti suggerisco di leggere il seguente articolo, che illustra approcci che sono imperfetti e descrive anche come eseguire correttamente il framebusting:

Applicazione del flusso di lavoro. Non ho compreso la tua discussione sull'assicurazione che checkout.php non dovrebbe essere accessibile prima di review.php . Non mi è chiaro il motivo per cui lo si elenca come proprietà di sicurezza o quale valore di sicurezza viene aggiunto. Non mi sembra un problema di sicurezza. Inoltre, il tuo meccanismo potrebbe rompere con l'apertura di più copie del tuo sito in più schede.

Se la tua preoccupazione riguarda attacchi di esplorazione forzati, ti suggerisco di assicurarti di utilizzare la difesa CSRF corretta in tutti i punti del sito, inclusa la pagina checkout.php e tutti i target dei moduli su quella pagina.

Difesa CSRF. Si noti che è necessario utilizzare un token CSRF per proteggere tutte le azioni che hanno effetti collaterali. Se il tuo sito è adeguatamente architettato, questo significa che un token CSRF dovrebbe essere usato per proteggere tutte le richieste POST.

Token CSRF. Usa mt_rand() per generare il token CSRF. Questo non è sicuro e rappresenta una cattiva pratica. È necessario utilizzare un PRNG crittograficamente valido (ad es., Letto da /dev/urandom ), per garantire che l'autore dell'attacco non possa prevedere questo valore. mt_rand() non è crittograficamente strong. Vedi, ad esempio, Un rand di glibc è sicuro per una chiave di accesso? , Quali sono i requisiti per un generatore di numeri casuali da utilizzare in sicurezza in crittografia? , È un rand di / dev / urandom sicuro per un codice di accesso? .

Inoltre, stai chiamando mt_rand(5,15) , il che significa che il tuo numero casuale è compreso nell'intervallo 5..15. In altre parole, hai meno di 4 bit di casualità. Questo è totalmente inadeguato e completamente rotto. Spero sia stato un refuso o forse ho frainteso.

Login fallito. Hai vietato l'accesso dell'utente per un mese dopo 10 tentativi di accesso falliti. Probabilmente non è una grande idea, perché rende troppo facile ai "griefer" bloccare un account fuori dal loro account per un mese. Suggerisco di leggere le seguenti domande su questo sito: Perché i siti implementano il blocco dopo 3 tentativi di password falliti? , Sta negando l'accesso dopo che i tentativi non corretti sono stati inefficaci? , Strategia appropriata per prevenire il forzamento bruto degli accessi? , Come posso proteggere la mia pagina di accesso? , Perché ricontrollare con CAPTCHA sulla voce di modulo non riuscita? .

Cookie. Assicurati di impostare il flag secure su tutti i cookie (in modo che vengano rispediti solo tramite le connessioni HTTPS). Ti suggerisco di abilitare HTTP Transport Transport Security, per prevenire attacchi in stile sslstrip.

Commento generale. Molte delle cose che stai costruendo sono standard di bog. In futuro potresti prendere in considerazione la scelta di un framework di sviluppo di applicazioni Web che supporti questi meccanismi (ad es. Gestione sicura delle sessioni, difesa CSRF, controlli di autorizzazione). Inoltre ti incoraggio a guardare le risorse di OWASP sulla sicurezza delle applicazioni web. Riguardano molto bene gli argomenti che stai chiedendo.

    
risposta data 28.04.2012 - 09:26
fonte

Leggi altre domande sui tag