UserPie, sicuro e "a passo coi tempi"?

Marco Bonanno

Utente Attivo
3 Lug 2012
32
0
6
Ciao ragazzi, ho chiesto un po in giro ma nessuno ha mai risposto, allora vengo alla mia questione. Sto utilizzando USerPie https://github.com/booruguru/UserPie, mi serve come base per costruire un mio script personale, gestisce quindi la registrazione/login ecc.

Chiedo a voi, lo script è sicuro? Non dico ovviamente una fortezza ma il minimo? La connessione è con mysqli e le password vengono criptate correttamente, almeno credo, con i giusti metodi.

Ma non vorrei che le funzioni per le query siano vecchie o che sia "scarso" contro attacchi SQL ignition.
Grazie
 
"Scarso" contro SQL Injection non sembra, anche se temo che sia per compatibilità col vecchio modulo mysql di php che non utilizza i prepared statements.

Vecchio purtroppo il codice sembra che lo sia abbastanza, è strano che in un contesto ad oggetti venga impiegato mysqli procedurale ad esempio oppure che ci siano rudimentali funzioni di building delle query sql o, ancora, che le funzioni procedurali utilizzino la direttiva global per accedere a delle risorse globali invece del vettore superglobale $GLOBALS.

La funzione che genera la crittazione delle password invece è tutto fuorché sicura. Si basa su sha1 (che al giorno d'oggi risulta debole come algoritmo di hashing) e utilizza dei salt la cui casualità non è crittograficamente robusta.

E' questo l'unico punto debole che mi sembra di osservare in una rapida occhiata.

Ti posso consigliare la lettura di questo articolo che ti può dare qualche spunto per aggiornare il sistema di crittazione delle password: https://www.mrw.it/sicurezza/php-password-login-sicuri_11755.html


Edit
Mi correggo, lo script basa tutta la sua sicurezza dalle injection sulle funzioni real_escape che da sole non sono sufficienti a garantire una sufficiente protezione, pertanto per quel che mi riguarda è aperto alla vulnerabilità.

C'è inoltre il fatto che sembrano passati anni dall'ultima volta che lo script è stato aggiornato, di conseguenza ti consiglio sinceramente di cercare qualche altra alternativa.
 
Ultima modifica:
Ciao e grazie per la tua più che soddisfacente risposta, a tal proposito ho cercato e trovato questo: https://daveismyname.com/login-and-registration-system-with-php-bp#.VRtpavmsUxt La connessione mi sembra di capire sia con PDO che nel poco della mia esperienza mi sembra di capire sia qualcosa di recente e quinti più che ottimo se si intende fare un progetto per il futuro, oltre che alla sicurezza si sono tutti i requisiti con password_hash. Sbaglio? Grazie ancora.
 
Ciao, lo script che hai trovato è un gestore utente veramente ridotto all'essenziale e, si, sembra ben realizzato questa volta.
E' sicuramente un ottima base di partenza ed ha meno pretese di far da CMS rispetto al precedente.

L'unica cosa che ti consiglio di fare è di modificare la direttiva display_errors di PHP perché, in alcune casistiche di fallimento, PDO potrebbe mostrare i dati di connessione attraverso i messaggi d'errore.

PHP:
ini_set('display_errors', 0);

Per il resto non dovresti avere particolari problemi di sicurezza se programmi il resto della tua applicazione realizzando le query prendendo spunto dagli esempi presenti in quel pacchetto ;-)
 
Ciao, potresti spiegarmi un po più dettagliatamente come fare? Grazie mille ancora :)

PS: Inoltre, ho provato a mostrare qualche dettaglio in piu nella memberpage.php ma oltre all'username non viene mostrato, credo si debba aggiungere qualcosa alla class user.php vero?
 
Ultima modifica:
Grazie mille, è l'unico forum in cui ricevo realmente degli aiuti, mentre ci sono allo abuso della tua gentilezza. Ho provato a mostrare qualche dettaglio in piu nella memberpage.php ma oltre all'username non viene mostrato, credo si debba aggiungere qualcosa alla class user.php per prelevare tutti i dati della tabella members vero?
 
Si, dovresti realizzare un metodo nella classe User che, a fronte di uno username fornito in input, ti ritorni dal database tutti dati di cui hai bisogno.

Successivamente dovresti valorizzare questi dati in sessione mediante la pagina di login (dove ti ho evidenziato dov'è che avviene l'assegnazione del valore relativo allo username per un esempio).
 
Dovrei aggiungere i valori che richiedo nella classe user come ad esempio qui:

Codice:
	public function login($username, $password){

		$hashed = $this->get_user_hash($username);
		
		if($this->password_verify($password,$hashed) == 1){
		    
		    $_SESSION['loggedin'] = true;
		    return true;
		} 	
	}

Anche se non credo, se non chiedo troppo potresti farmi un esempio pratico?
Scusami... :)
 
Un esempio potrebbe essere un metodo del genere nella classe User:
PHP:
    public function get_user_info($username){	
        try {
            $stmt = $this->_db->prepare('SELECT memberID, email, active FROM members WHERE username = ?');
            $stmt->execute(array($username));
			
            return $stmt->fetch();

        } catch(PDOException $e) {
            echo '<p class="bg-danger">'.$e->getMessage().'</p>';
        }
    }

Con un utilizzo simile nella pagina di login:
PHP:
    if($user->login($username,$password)) { 

        // Recuperiamo quindi le informazioni dell'utente che si è appena collegato
        $user_info = $user->get_user_info($username);
        // $user_info è ora un array che contiene le informazioni in chiavi corrispondenti alle colonne selezionate con la query

        $_SESSION['username'] = $username;

        // e valorizziamo le informazioni che mi interessano nella sessione
        $_SESSION['member_id'] = $user_info['memberID'];
        $_SESSION['email'] = $user_info['email'];
        $_SESSION['active'] = $user_info['active'];

        header('Location: memberpage.php');
        exit;

    } else {
        $error[] = 'Wrong username or password or your account has not been activated.';
    }

In ultimo, nella tua memberpage puoi finalmente mostrare le informazioni ottenute:
PHP:
            <h2>Member only page - Welcome <?php echo $_SESSION['username']; ?></h2>
            <p>Your email is <?php echo $_SESSION['email']; ?></p>
            <p><a href='logout.php'>Logout</a></p>
            <hr>
 
Ultima modifica:
Wow, perfetto, grazie mille ancora una volta. Sto provato a creare una pagina profilo per tutti gli utenti, il codice che ho fatto sarebbe questo:

Codice:
<?php require('includes/config.php'); 

$id = $_GET['id']; // ID user

$sql = $db->prepare("SELECT * FROM members where memberID = '$id'");
if ($sql->execute(array($_GET['memberID']))) {
  while ($row = $sql->fetch()) {

//define page title
$title = 'Profile';

//include header template
require('layout/header.php'); 
?>

<div class="container">
    <h2><?php echo $row['username']; ?></h2>
    <h2><?php echo $row['email']; ?></h2>
</div>

<?php 
  }
}
?>

Funziona, ad esempio andando su profile.php?id=1
Ma mi chiedevo se la sintassi fosse corretta?
Chiedo questo perche ho provato delle guide:
http://php.net/manual/en/pdo.prepared-statements.php
https://www.mrw.it/php/guida-utilizzo-pdo_7594.html

Solo che ho trovato soluzione solo con quelle illustrate da php.net, cosa cambia?
 
Non cambia fondamentalmente nulla, gli esempi sono gli stessi solo che, nel modo in cui hai realizzato la query, in quella pagina ti esponi al rischio injection.

I prepared statements sono fondamentalmente un metodo che permette di inserire dati nella query sfruttando dei "segnaposto".
Nel caso di PDO ce ne sono di due tipi, ma proverò a spiegarti quello di più semplice utilizzo a mio avviso:

PHP:
// Questa è la query che hai realizzato, hai inserito $id nella composizione della query senza alcun controllo, quindi ti sei aperto alla vulnerabilità
$sql = $db->prepare("SELECT * FROM members where memberID = '$id'");

// La forma corretta per realizzare in sicurezza la query è la seguente:
$sql = $db->prepare("SELECT * FROM members where memberID = ?");

// Il punto di domanda è il famoso "segnaposto" e in una query di questo tipo ne puoi inserire quanti ne vuoi
// I dati che verranno sostituiti ai segnaposto vanno passati in ordine al comando che poi spedisce effettivamente la query al database:
$sql->execute(array($id));

// Se la tua query avesse avuto due variabili contrassegnate dal punto di domanda, avresti dovuto passare i dati all'execute in ordine di come compaiono i segnaposto:
// es: $sql->execute(array($dato1, $dato2));

// A questo punto, la query è stata eseguita, ma bisogna recuperare i dati.
// PDO mette a disposizione 2 metodi:  fetch() e fetchAll()
// La differenza è che il primo è utile quando ti aspetti di ritornare un unica riga dal database (ad esempio nel tuo caso che stai selezionando le informazioni relative ad un unico utente) la seconda invece ti ritorna le informazioni relative a tutte le righe in un array multidimensionale.

// nel tuo caso:
$row = $sql->fetch();

// Da $row adesso puoi tranquillamente estrapolare 'username', 'email' e tutto ciò di cui hai bisogno.

// ponendo il caso invece che dovevi realizzare una query che ti ritorni uno o più risultati, avresti dovuto fare così:
/* es: 
$rows = $sql->fetchAll();

if (!empty($rows)) {

    // Ho trovato risultati, ora li scorro riga per riga
    foreach ($rows as $row) {
        
        // A questo punto $row contiene le informazioni della riga corrente
        
    }

} else {
    echo 'Non ho trovato risultati dalla query';
}

*/

La documentazione di PHP infine ti spiega tutte le possibilità accessorie che ti offrono i metodi di ritorno dei dati coinvolti:

PDOStatement::fetch
PDOStatement::fetchAll
 
Grazie mille, tutto perfetto, mi stai aiutando molto a capire come funziona. Ho infatti provato a creare anche una pagina dei membri registrati, con successo.

Vorrei provare a creare anche una pagina per modificare la password e successivamente per caricare il proprio avatar. Volevo domandarti, dovrei creare una funzione nella classe user, oppure posso eseguire la query nella stessa pagina, "changepassword.php", come nell'esempio fatto qui sopra da te? (ovviamente con UPDATE).
 
Il modo più pulito di procedere è di realizzare un metodo nella classe User che si occupi di eseguire la query di aggiornamento, mentre nella nuova pagina gestisci solo il passaggio dei dati dal form al metodo che hai realizzato.

Il consiglio è valido anche a livello generale per tutto quello che riguarda un aspetto logico della tua applicazione.

Salvo esigenze veramente particolari infatti, nelle pagine che scrivi non dovresti mai inserire delle query, pertanto potresti dover prendere in considerazione anche l'idea di scrivere nuove classi per gestire eventuali nuovi aspetti della tua applicazione che si occupino di racchiudere a livello logico le procedure ivi associate.

Caso d'esempio: se mai avessi l'esigenza di dover creare uno strumento di messaggistica interna al tuo sito tra i tuoi utenti, ti consiglierei di racchiudere tutte le operazioni che riguardino questo aspetto in una ipotetica classe "Messenger" e quindi nelle tue pagine usufruire dei metodi di questo nuovo oggetto anziché scrivere manualmente le query ogni volta.

I vantaggi nel procedere in questo modo si riflettono nella riduzione del codice delle tue pagine che ti comporta, di conseguenza, anche una più semplice lettura dello stesso (ti servono meno righe per scrivere la funzionalità di una data pagina) , nel riutilizzo del codice che hai già scritto (se due pagine hanno funzionalità simili potresti facilmente prendere in considerazione di usare gli stessi metodi - ad esempio potresti pensare di sostituire la query scritta nella pagina che hai creato col metodo get_user_info della classe user per recuperare quelle info ;) ), ma soprattutto ti sarà più semplice mantenere/aggiornare il codice in futuro nel caso tu debba scrivere nuove funzionalità.
 
Ultima modifica:
Purtroppo non ci sono molte guide pratiche, devo capire un po meglio perchè al momento so solo mettere mani solo sul codice esistente. Mi stavo cimentando nella creazione di un accesso solo admin (praticamente la stessa cosa del contenuto visibile solo all'utente loggato, vorrei fare che un contenuto venga visualizzato solo ad utenti con i diversi livelli).

Ogni utente ha il livello 1, mentre admin 2.

Evito di creare la classe con i vari livelli, e credo semplicemente il campo nel database.
Quindi prendendo uno spunto della funzione per gli utenti loggati, come potrei fare?

Codice:
public function is_logged_admin(){
		if(isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true){
			return true;
		}		
	}
 
Hai intuito bene, i livelli è bene gestirli mediante il database.

Una volta che la tua tabella utenti possiede la nuova colonna che contiene il livello di accesso, puoi recuperarla col metodo get_user_info e poi inserirne il risultato in sessione, esattamente come fatto per gli altri dati.

A questo punto il controllo su "is_logged_admin" lo basi sul nuovo valore di sessione che hai incamerato ;)
 
Potresti farmi un esempio pratico della struttura da inserire nella classe?

Codice:
public function is_logged_admin(){
		if(isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true){
			return true;
		}		
	}


Per il resto, credo di riuscirci da solo. Esistono magari delle classi già pronte o comunque dove orientarsi sulla corretta scrittura?
 
Classi già pronte non saprei, per quanto riguarda l'implementazione devi semplicemente tornare indietro al metodo get_user_info e aggiungere la nuova colonna da recuperare nella query che è già presente.

Nel login invece prendi il nuovo valore, similiarmente agli altri, e lo metti in sessione.

A questo punto, il controllo lo puoi realizzare direttamente sul valore memorizzato in sessione, ad esempio:

PHP:
if ($_SESSION['livello'] == 2) {
    // ok, sei admin
}
 
Alla fine ho fatto in questo modo:
Nella classe user.php ho scritto questo:

Codice:
public function is_logged_admin(){
		if(isset($_SESSION['level']) && $_SESSION['level'] == 2 ){
			return true;
		}		
	}

E poi ovviamente nelle pagine che voglio proteggere farei cosi,

Codice:
<?php if ($user->is_logged_admin()): ?>
Contenuto se solo admin, livello 2
<?php endif; ?>

Solo vorrei proteggere l'intera pagina, come una pagina amministrativa, che l'utente quindi se non è ll'admin viene reindirizzato ad un altra pagina.

farei cosi ma non va correttamente,

if ($user->is_logged_admin()) {
header('Location: login.php');
}


Per il resto non riesco a costruire la pagina del cambio password, sfortunatamente!
 
Presumendo che nella pagina di login tu abbia correttamente valorizzato $_SESSION['level'], il tuo metodo dovrebbe essere giusto come ragionamento, ma è meglio fargli restituire esplicitamente false quando la condizione non è veritiera:
PHP:
    public function is_logged_admin(){
        if(isset($_SESSION['level']) && $_SESSION['level'] == 2 ){
            return true;
        }
        
        return false;
    }

Nelle pagine che vuoi proteggere, puoi applicare il ragionamento inverso rispetto a quello che hai fatto:
PHP:
<?php 
    // Se lo user connesso NON dispone di permessi da amministratore
    if (!$user->is_logged_admin()) {
        // ferma l'esecuzione dello script
        exit();
    }
?>
Contenuto pagina da proteggere..

Quanto al redirect..
PHP:
header('Location: login.php');
..devi considerare due fattori importanti:

Il primo è che non puoi inviare header al browser se è già partito un qualsiasi output dallo script (anche uno spazio bianco per intenderci, in quanto questo fa si che php invii tutte le intestazioni di default, ignorando quindi successive richieste), di conseguenza questo controllo è un operazione che dovresti fare in una porzione di script posto ad inizio file e inoltre devi assicurarti che in eventuali files inclusi precedentemente (oltre a non esserci echo o print di sorta) il tag di apertura <?php non abbia nessuno spazio a precederlo.
Nel caso sia presente un qualsiasi output il redirect non funzionerà.

La seconda è che anche se invii il tag header per effettuare il redirect con successo, il tuo script continuerà l'esecuzione, di conseguenza potrebbe non essere molto difficoltoso per un possibile maleintenzionato ignorare l'header e visualizzare comunque la pagina protetta. E' importante quindi terminare l'esecuzione dello script dopo ogni redirect.

A fronte di queste informazioni, per come la vedo io, il modo più pratico per gestire il redirect utente è scrivere una funzione che si occupi di risolvere autonomamente questo tipo di problematiche:
PHP:
function redirect($url) {
    // verifico che sia ancora possibile spedire header al browser
    if (!headers_sent()) {

        // e quindi ne invio uno per effettuare il redirect
        header("Location: {$url}", true);

    } else {

        // se non posso mandare header, allora uso javascript per il redirect
        echo "<script>self.location.href='{$url}';</script>";

    }

    // termino lo script, in quanto ora è compito del client soddisfare il redirect
    exit();
}

Questa funzione ti consiglio di salvarla in un nuovo file, magari chiamato functions.php, e posizionarlo nella cartella includes.
A questo punto dovrai solo includere il file dove ti occorre quella funzione, inoltre nello stesso ti sarà semplice aggiungere nuove funzioni qualora ti occorrano, permettendoti quindi di riutilizzare quelle definizioni ovunque ne necessiti.

Sfruttando la funzione che ti ho scritto, il tuo controllo finale potrebbe diventare tranquillamente qualcosa del genere:
PHP:
<?php 
    // Se lo user connesso NON dispone di permessi da amministratore
    if (!$user->is_logged_admin()) {
        // redireziona l'utente e ferma l'esecuzione dello script
        redirect('login.php');
    }
?>
Contenuto pagina da proteggere..
 
Ultima modifica:

Discussioni simili