Cenni sulla sicurezza delle Servlet

annuncio pubblicitario
Cenni sulla Sicurezza delle
Servlet
Dispense per il corso di Ingegneria del Web
Revisione 06/11
Giuseppe Della Penna ([email protected])
Dipartimento di Informatica
Università degli studi dell'Aquila
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie form
Cenni sulla sicurezza delle Servlet
In questa lezione vedremo una serie di frammenti di codice e funzioni utili per programmare
vere applicazioni web, con un occhio alla sicurezza. Il codice che vi mostrerò implementa dei
comuni "trucchi" che forniranno un'utile base di sicurezza alle vostre applicazioni.
Le funzioni (statiche) proposte fanno parte di una classe SecurityHelpers, il cui sorgente può essere scaricato
unitamente a un'applicazione web che propone una serie di servlet di esempio, dal materiale web associato al
corso. Attenzione: l'applicazione web utilizza un database (la cui semplice struttura è deducibile dal codice),
che dovrete creare se volete vederla in funzione, e contiene anche altre semplici servlet di "esempio pratico".
Mettere in sicurezza le proprie form
Quando un utente invia dei dati tramite una stringa di query o nel payload di un POST,
solitamente ma non necessariamente generati da una form, può immettere volontariamente o
involontariamente delle combinazioni di caratteri che possono disturbare l'esecuzione della
nostra servlet, o mettere in pericolo la nostra applicazione web.
Un caso molto comune è la SQL injection. In questo caso, l'utente malizioso inserisce come
valore di una variabile della form una stringa contenente caratteri speciali come virgolette, apici o
backslash. Se il programmatore costruisce una stringa di query con all'interno questi valori,
l'espressione risultante potrebbe essere interpretata dal database in maniera inattesa.
Ad esempio, se una servlet attende due parametri, username e password, e poi li compone nella
query che segue per verificare se le credenziali dell'utente sono valide
SELECT * FROM utenti WHERE username='"+username+"' AND
password='"+password+"'"
L'utente potrebbe fornire come username e password la stringa "xyz' OR 'x'='x" (notate l'uso degli
apici), e a questo punto la stringa di query risultante diventerebbe
SELECT * FROM utenti WHERE username='xyz' OR 'x'='x' AND password='xyz' OR
'x'='x'"
La query restituirebbe sempre almeno un record, per via della tautologia 'x'='x' iniettata nella
stringa, e il nostro codice potrebbe interpretare questo come certificazione della validità delle
credenziali!
Per ovviare a questo, è necessario inserire degli escape (\) prima di tutti i caratteri "insicuri"
all'interno dei parametri prelevati dalla HttpServletRequest. Questo può essere ottenuto usando
la seguente funzione addSlashes (derivata da PHP):
//questa funzione aggiunge un backslash davanti a
//tutti i caratteri "pericolosi", usati per eseguire
//SQL injection attraverso i parametri delle form
public static String addSlashes(String s) {
return s.replaceAll("(['\"\\\\])", "\\\\$1");
}
La query vista sopra andrebbe quindi costruita come segue
Revisione 06/11
Pagina 2/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie password
SELECT * FROM utenti WHERE
username='"+SecurityHelpers.addSlashes(username)+"' AND
password='"+SecurityHelpers.addSlashes(password)+"'"
Inoltre, quando preleviamo dal database dei dati ai quali era stata applicata la funzione
addSlashes in fase di inserimento, dobbiamo ricordarci di rimuovere gli escape usando la funzione
stripSlashes:
//questa funzione rimuove gli slash aggiunti da addSlashes
public static String stripSlashes(String s) {
return s.replaceAll("\\\\(['\"\\\\])", "$1");
}
Infine, è sempre opportuno fare in modo che i parametri siano di tipo numerico, ove possibile.
Infatti i numeri sono semplici da riconoscere e totalmente innocui se immessi nelle query. Se
sappiamo che un parametro dovrà essere numerico, possiamo "filtrarlo" facilmente applicando la
funzione checkNumeric:
public static int checkNumeric(String s) throws NumberFormatException {
//convertiamo la stringa in numero, ma assicuriamoci prima che sia valida
if (s != null) {
//se la conversione fallisce, viene generata un'eccezione
return Integer.parseInt(s);
} else throw new NumberFormatException("String is null");
}
La funzione solleva un'eccezione se l'argomento non è un vero numero, altrimenti restituisce il
numero stesso come intero.
Mettere in sicurezza le proprie password
In questa sezione presenteremo un semplice esempio di procedura di registrazione utenti, e la
corrispondente procedura di login. Sfrutteremo anche alcuni dei sistemi di sicurezza appena
illustrati. Gli esempi saranno compatti, inseriti in un singolo blocco di codice. Non si tratta di buona
programmazione da imitare, ma solo di un modo per darvi un'idea completa in maniera semplice e
rapida.
Vorrei attrarre la vostra attenzione sul modo in cui le password vengono scritte e
successivamente confrontate nel database, usando una codifica non invertibile come l'MD5 per
assicurarsi che nessuno, neppure accedendo al database, possa rubare le nostre password e
successivamente riutilizzarle nel sito.
Il codice che segue illustra la procedura di registrazione. Si assume di avere una Configuration di
Freemarker, chiamata cfg e una connessione al database, chiamata c, già configurati nella nostra
classe. Si assume inoltre di avere un template come quello che segue per visualizzare il form di
registrazione e i messaggi associati:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Esempi Vari</title>
<link rel="stylesheet" href="style/default.css" type="text/css"/>
</head>
<body>
Revisione 06/11
Pagina 3/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie password
<h1>Registrazione</h1>
<#if messaggio??>
<p><b>${messaggio}</b></p>
</#if>
<p>Inserisci i tuoi dati e clicca su "registrami"</p>
<form method="post" action="Registrazione">
<p>Username: <input name="nome" type="text"/></p>
<p>Password: <input name="pass1" type="password"/></p>
<p>Password: <input name="pass2" type="password"/></p>
<p><input value="Registrami" name="registra" type="submit"/></p>
</form>
</body>
</html>
Notare che è presente un'area riservata ai messaggi, che viene riempita dal codice nel caso ci
siano segnalazioni (di errore o successo) da fare.
protected void processRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
//Freemarker: prepariamo il data model
Map data = new HashMap();
//Freemarker: carichiamo il template
Template t = cfg.getTemplate("registrazione.ftl.html");
try {
//se abbiamo premuto il bottone di registrazione
if (request.getParameter("registra") != null) {
//preleviamo i parametri della form di registrazione
String nome = request.getParameter("nome");
String pass1 = request.getParameter("pass1");
String pass2 = request.getParameter("pass2");
//verifichiamo che i parametri obbligatori siano stati inseriti
if (pass1 != null && pass2 != null && nome != null && !nome.isEmpty()) {
//verifichiamo la corrispondenza tra le due copie della password
if (pass1.equals(pass2)) {
//creiamo uno statement per le nostre query
Statement s = c.createStatement();
//verifichiamo se la username è già in uso
//notare l'uso della funzione addSlashes
//per inserire i backslash davanti ai caratteri "pericolosi"
ResultSet r = s.executeQuery("SELECT id FROM utente WHERE username='"
+ SecurityHelpers.addSlashes(nome) + "'");
//se la query non restituisce alcun record...
if (!r.next()) {
//inseriamo il record per il nuovo utente
//notare l'uso della funzione MD5 per codificare la password nel db
s.executeUpdate("insert into utenti(username,password) VALUES ('" +
SecurityHelpers.addSlashes(nome) + "',md5('" +
SecurityHelpers.addSlashes(pass1) + "'))");
//mostriamo un messaggio di successo
data.put("messaggio", "registrazione eseguita con successo");
r.close();
s.close();
} else {
data.put("messaggio", "username già presente");
Revisione 06/11
Pagina 4/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie password
}
} else {
data.put("messaggio", "le password non corrispondono");
}
} else {
data.put("messaggio", "parametri non specificati");
}
} else {
//nulla
}
//Freemarker: compiliamo il template
t.process(data, out);
} catch (TemplateException ex) {
throw new ServletException("Errore nell'elaborazione del template: " +
ex.getMessage(), ex);
} catch (SQLException ex) {
throw new ServletException("Errore nell'accesso ai dati: " +
ex.getMessage(), ex);
}}
Da notare tutti i controlli, ognuno dei quali in caso di fallimento provoca la visualizzazione della
stessa form con un messaggio di errore. In caso di successo, la form viene ripresentata (ma si
potrebbe anche fare una redirect o caricare un template differente) con un messaggio opportuno.
Il codice che segue illustra la corrispondente procedura di login. Prima di tutto il template:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html
xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Esempi Vari</title>
<link rel="stylesheet" href="style/default.css" type="text/css"/>
</head>
<body>
<h1>Login</h1>
<#if messaggio??>
<p><b>${messaggio}</b></p>
</#if>
<#if !logged>
<p>Inserisci i tuoi dati e clicca su "Login"</p>
<form method="post" action="Login">
<p>Username: <input name="nome" type="text"/></p>
<p>Password: <input name="pass" type="password"/></p>
<p><input value="Login" name="login" type="submit"/></p>
</form>
<#else>
<p><a href="Homepage">Vai alla homepage</a></p>
</#if>
</body>
</html>
E' presente l'area messaggi, come pere il form di registrazione. Il modulo di login viene sostituito
da un link alla homepage in caso di login completato con successo. Ecco il codice corrispondente:
protected void processRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
HttpSession sess = request.getSession(true);
Revisione 06/11
Pagina 5/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie password
PrintWriter out = response.getWriter();
//Freemarker: prepariamo il data model
Map data = new HashMap();
data.put("logged", false);
//Freemarker: carichiamo il template
Template t = cfg.getTemplate("login.ftl.html");
try {
//se abbiamo premuto il bottone di login
if (request.getParameter("login") != null) {
//preleviamo i parametri della form
String nome = request.getParameter("nome");
String pass = request.getParameter("pass");
//verifichiamo che i parametri obbligatori siano stati inseriti
if (pass != null && nome != null && !nome.isEmpty() && !pass.isEmpty()) {
//creiamo uno statement per le nostre query
Statement s = c.createStatement();
//preleviamo i dati associati alla username/password dati
//notare l'uso della funzione addSlashes per inserire i
//backslash davanti ai caratteri "pericolosi"
ResultSet r = s.executeQuery("SELECT * from utenti WHERE username='" +
SecurityHelpers.addSlashes(nome) + "' AND password =md5('" +
SecurityHelpers.addSlashes(pass) + "')");
//se esiste un record utente corrispondente
if (r.next()) {
//settiamo gli attributi della sessione
//notare l'uso della funzione stripSlashes per eliminare
//gli slash di sicurezza presenti nei dati del DB
//e aggiunti dalla funzione addSlashes
sess.setAttribute("username",
SecurityHelpers.stripSlashes(r.getString("username")));
sess.setAttribute("ip", request.getRemoteHost());
sess.setAttribute("inizio-sessione", Calendar.getInstance());
sess.setAttribute("userId", r.getInt("ID")); data.put("logged", true);
//mostriamo un messaggio di successo
data.put("messaggio", "login avvenuto con successo");
} else {
data.put("messaggio", "username o password non valide");
}
r.close();
s.close();
} else {
data.put("messaggio", "parametri non specificati");
}
} else {
//nulla
}
//Freemarker: compiliamo il template
t.process(data, out);
} catch (TemplateException ex) {
throw new ServletException("Errore nell'elaborazione del template: " +
ex.getMessage(), ex);
} catch (SQLException ex) {
throw new ServletException("Errore nell'accesso ai dati: " +
ex.getMessage(), ex);
}}
La logica è simile alla procedura di registrazione, con una serie di controlli che generano messaggi
corrispondenti. In caso di successo, poniamo a true la variabile logged nel data model per
Revisione 06/11
Pagina 6/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie sessioni
provocare la comparsa del link alla homepage, e inizializziamo le strutture usate poi dalla funzione
checkSession, che vedremo nella prossima sezione.
Mettere in sicurezza le proprie sessioni
Abbiamo già visto come controllare, in una servlet, se la sessione corrente è autenticata. Tuttavia
questo non basta a prevenire molti attacchi che mirano a "rubare" la sessione di un utente e
spacciarsi per lui. A questo scopo, ogni servlet dovrebbe eseguire una serie di controlli sulla
sessione corrente, prima di ritenerla valida e attiva, e poi decidere in base alla validità della
sessione stessa come procedere.
La funzione checkSession che segue è proprio quello di cui abbiamo bisogno
/questa funzione esegue una serie di controlli di sicurezza
//sulla sessione corrente. Se la sessione non è valida, la cancella
//e ritorna null, altrimenti la aggiorna e la restituisce
public static HttpSession checkSession(HttpServletRequest r) {
boolean check = true;
HttpSession s = r.getSession(false);
//per prima cosa vediamo se la sessione è attiva
if (s == null) return null;
//check sulla validità della sessione
if (s.getAttribute("userId") == null) {
check = false;
//check sull'ip del client
} else if (!((String) s.getAttribute("ip")).equals(r.getRemoteHost())) {
check = false;
//check sulle date
} else {
//inizio sessione
Calendar begin = (Calendar) s.getAttribute("inizio-sessione");
//ultima azione
Calendar last = (Calendar) s.getAttribute("ultima-azione");
//data/ora correnti
Calendar now = Calendar.getInstance();
if (begin == null) {
check = false;
} else {
//millisecondi trascorsi dall'inizio della sessione
long secondsfrombegin = (now.getTimeInMillis() begin.getTimeInMillis()) / 1000;
//dopo tre ore la sessione scade
if (secondsfrombegin / 60 > 3 * 60) {
check = false;
} else if (last != null) {
//millisecondi trascorsi dall'ultima azione
long secondsfromlast = (now.getTimeInMillis() - last.getTimeInMillis())
/ 1000;
//dopo 30 minuti dall'ultima operazione la sessione è invalidata
if (secondsfromlast / 60 > 1 * 60) {
check = false;
}
}
}
}
if (!check) {
s.invalidate();
return null;
Revisione 06/11
Pagina 7/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie connessioni
} else {
//reimpostiamo la data/ora dell'ultima azione
s.setAttribute("ultima-azione", Calendar.getInstance());
return s;
}}
L'idea è chiamare checkSession all'inizio della processRequest di ogni servlet (anche di quelle
accessibili a tutti, per garantire che le informazioni della sessione siano sempre aggiornate).
Dopodiché, se ci interessa determinare se un utente è connesso, possiamo utilizzare la
HTTPSession ritornata dalla funzione: se non è nulla, allora c'è un utente correttamente
identificato che agisce sul sistema. La funzione checkSession controlla molti aspetti legati al furto
delle credenziali: ad esempio, la sessione viene cancellata se l'accesso è compiuto da un indirizzo
IP diverso da quello del login, oppure se il tempo trascorso dal login o dall'ultima operazione è
troppo lungo. Si potrebbe inoltre modificare la funzione per eseguire ulteriori controlli di validità
sull'userId presente nella sessione (consultando il database degli utenti). Notate che la funzione
assume la presenza delle stesse variabili di sessione impostate dalla procedura di login già
illustrata a lezione (e presente nel caso di studio).
Una semplice servlet accessibile solo ad utenti riconosciuti dovrebbe quindi sfruttare questa
funzione come segue:
protected void processRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
//preleviamo la session e cirrente e ne controlliamo
//la validità rispetto ai criteri fissati
HttpSession s = SecurityHelpers.checkSession(request);
if (s == null) {
//la sessione non esiste o non è valida
//possiamo ridirigere su una pagina "sicura"
response.sendRedirect("Homepage");
//oppure potremmo mostrare un messaggio di errore
//o ancora mostrare la pagina con contenuti ridotti
} else {
PrintWriter out = response.getWriter();
//qui comincia la pagina...
}
}
Mettere in sicurezza le proprie connessioni
A volte è consigliabile accedere a una pagina usando una connessione HTTPS, magari perchè su
quella pagina (o nelle risorse ad essa collegate) sono presenti dati che non vogliamo siano
trasmessi in chiaro sulla rete. Con una connessione HTTPS, i dati saranno cifrati in maniera sicura.
Creare una connessione HTTPS è semplice: basta cambiare il protocollo della corrispondente URL
dal solito "HTTP" in "HTTPS". Se il server remoto supporta questo tipo di connessioni sicure (il che
non è automatico: il supporto HTTPS va solitamente attivato sui server con una procedura
specifica, magari scaricando opportune librerie di supporto), qualsiasi pagina che vi viene servita
con HTTP potrà essere servita anche via HTTPS semplicemente cambiando il protocollo nella URL.
Il codice del server non cambia, perché la cifratura avviene nel server stesso, in maniera
trasparente alle servlet.
Revisione 06/11
Pagina 8/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie connessioni
Tuttavia, se volete che una servlet sappia se il client le si è connesso in modalità HTTPS, potete
usare il codice che segue:
//questa funzione verifica se il protocollo HTTPS è attivo
public static boolean checkHttps(HttpServletRequest r) {
return r.isSecure();
//metodo alternativo "fatto a mano"
//String httpsheader = r.getHeader("HTTPS");
//return (httpsheader != null && httpsheader.toLowerCase().equals("on"));
}
Il codice si limita a usare il metodo isSecure della classe HttpServletRequest, che restituisce true
se la connessione corrente è crittografata (sicura). E’ anche possibile utilizzare un metodo
indipendente dal framweork delle servlet controllando se esiste nella richiesta l'intestazione
"HTTPS=ON", che indica l'attivazione del protocollo HTTPS. A questo punto, se la servlet decide
che l'utente dovrebbe essere connesso in HTTPS e non HTTP, può forzare il browser a ricaricarla
cambiando protocollo tramite il codice seguente
//questa funzione ridirige il browser sullo stesso indirizzo
//attuale, ma con protocollo https
public static void redirectToHttps(HttpServletRequest r,
HttpServletResponse resp) throws IOException {
//estraiamo le parti della request url
String server = r.getServerName();
//ATTENZIONE: per alcune configurazioni del server, potrebbe essere
//necessario cambiare il numero della porta
int port = r.getServerPort();
String context = r.getContextPath();
String path = r.getServletPath();
String info = r.getPathInfo();
String query = r.getQueryString();
//riconstruiamo la url cambiando il protocollo
String newUrl = "https://" + server + ":" + port + context + path + (info
!= null ? info : "") + (query != null ? "?" + query : "");
//ridirigiamo il client resp.sendRedirect(newUrl);
}
In pratica, ricostruiamo la URL corrente dalle sue componenti, cambiamo il protocollo, e
inviamo una redirect al browser (dopo la chiamata a questa funzione la servlet dovrebbe
terminare la propria esecuzione). E’ importante notare che, a seconda della configurazione del
server utilizzato, potrebbe essere necessario cambiare il numero di porta (variabile port) oltre al
protocollo. Notate che questo sistema non permette di ricaricare correttamente la pagina se
questa utilizza parametri passati in modalità POST (ma preserva i parametri GET).
Una semplice servlet accessibile solo tramite protocollo HTTPS dovrebbe quindi sfruttare queste
due funzioni come segue:
protected void processRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
//controlliamo se siamo in HTTPS
if (!SecurityHelpers.checkHttps(request)) {
//se non lo siamo chiediamo al browser di aprire
// questa stessa pagina col nuovo protocollo
SecurityHelpers.redirectToHttps(request, response);
} else {
PrintWriter out = response.getWriter();
//qui comincia la pagina...
Revisione 06/11
Pagina 9/10
Corso di Ingegneria del Web
Sicurezza delle Servlet: Mettere in sicurezza le proprie connessioni
}
}
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported
License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a
letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
Revisione 06/11
Pagina 10/10
Scarica