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