Tutto quello che può andare male in un form

Come sviluppatori, sappiamo che in alcuni utenti risiede nascosto qualcosa di malvagio. Al rilascio di un programma, per quanto il suo impiego possa essere semplice, c’è sempre quell’utente che al primo contatto riesce a scatenare quella casistica, sfuggita ai numerosi controlli ed all’attento team di tester, che provoca un crash, l’inserimento di dati incoerenti o la visualizzazione di messaggi inconsistenti. Anche per sviluppatori esperti, non sempre è facile prevedere tutte le possibili problematiche. Lo scopo di questo articolo è quello di elencare le varie situazioni a cui fare attenzione per implementare una serie di controlli che consenta al programma di comportarsi correttamente di fronte ad ogni tipo di inserimento dati da parte di un’utente.

La maggior parte di quanto riportato vale in molteplici contesti applicativi, anche se qualche approfondimento aggiuntivo viene considerato per applicazioni web C# MVC. Per molti programmi, implementare tutti questi controlli può essere eccessivo – sicuramente è però necessario fare un’accurata analisi di quali problematiche siamo disposti a tollerare e a quali invece dobbiamo assolutamente porre rimedio.

Stringhe

Inserimento di stringhe: l’utente si trova davanti una casella di testo dove può inserire qualsiasi combinazione di caratteri. Cosa può andare storto?

Lunghezza

Per qualsiasi tipo di dato in ingresso, è sempre necessario un controllo sulla sua grandezza. Se la stringa inserita è più grande del campo del database, la sottomissione potrebbe fallire e provocare il rigetto dell’intero form. Il problema potrebbe essere mitigato, in prima battuta, impostando sul DB campi per stringhe molto grandi; assicurarsi che siano a lunghezza variabile, in modo da non sprecare grandi quantità di spazio su disco.

Un altro punto insidioso: alcuni caratteri possono richiedere più di un byte per essere memorizzati, e questo può creare confusione se il limite di lunghezza viene mostrato all’utente (es. con un messaggio ‘hai ancora xxx caratteri a disposizione’). Per questo caso, è raccomandabile impostare una lunghezza del campo del DB maggiore di quella dettata all’utente.

Spazi vuoti

Gli spazi sono invisibili all’utente, ma senza un’azione esplicita un programma li prenderà in considerazione. La ricerca di una persona con nome ‘Mar’ potrebbe dare più risultati della ricerca di ‘Mar ‘. Un nome utente con uno spazio finale potrebbe rendere impossibile il login, se al momento dell’autenticazione lo stesso nome viene inserito senza spazio. Un campo di ricerca contenente unicamente uno spazio potrebbe aggiungere vincoli alla ricerca del tutto invisibili dall’interfaccia utente.

Molto spesso, da ogni stringa in ingresso dovrebbero essere rimossi gli spazi a inizio e a fine, ed un testo contenente solo spazi (o altri caratteri come tab, invio, ecc…) dovrebbe essere considerato vuoto.

Encoding

La vostra applicazione supporta una codifica che copre anche caratteri speciali? Per rendere il programma disponibile in tutto il mondo, è necessario supportare set di caratteri molto grandi – solitamente l’UTF8 risolve il problema, ma è necessario che tutte le componenti coinvolte nel flusso dati siano configurate in modo da supportarlo. Queste comprendono le pagine html generate, il database, la relativa connessione e il suo driver, eventuali API esterne utilizzate, file DOC/PDF/XLS generati a partire da dati inseriti dall’utente, ecc…

Pattern specifici

Altri tipi di stringhe possono richiedere la corrispondenza con un pattern specifico. Un numero di telefono potrebbe essere formato, per esempio, solo da caratteri numerici, spazi e dal carattere ‘+’ per i prefissi internazionali. Può essere interessante sapere che, lato client, si può utilizzare l’attributo ‘pattern’ (HTML5) per validare una stringa contro un’espressione regolare senza l’uso di Javascript.

Numeri

Iniziamo con una buona notizia: nell’HTML 5 sono stati inseriti nuovi tipi di input che consentono di limitare dal browser stesso il tipo di dato in ingresso. Per i numeri abbiamo il tipo number; altre opzioni utili sono i tipi color, date, email, month, time. Questo non toglie che una validazione lato server è comunque necessaria, per esempio per i browser più vecchi che non supportano il nuovo standard.

Intervalli

A ben pensarci, qualsiasi numero inserito dall’utente deve cadere in un certo intervallo. Molto spesso solo numeri non negativi hanno senso. In alcuni casi un numero negativo nel posto sbagliato può rappresentare persino un problema di sicurezza – pensate ad un’applicazione dove tramite un form un utente può regalare un numero di punti fedeltà a sua scelta ad un altro utente.

Anche per il massimo si possono spesso individuare limiti ragionevoli, per esempio se il numero rappresenta una percentuale, un’età, un anno, un periodo temporale. Ricordarsi infine che ogni numero rappresentato su una macchina ha una grandezza massima: come reagisce l’applicazione davanti ad un overflow?

Decimali

È importante anche pensare a quando è sensato che un numero possa avere anche cifre decimali; se si sceglie di usare un intero, segnalare all’utente il giusto messaggio se inserisce un numero con la virgola.

Se si sceglie di accettare numeri non interi, ricordarsi delle diverse codifiche: il separatore decimale, a seconda della cultura dell’utente, può essere un punto o una virgola. Ancora peggio, in alcune culture il punto può essere utilizzato come separatore delle migliaia (es. 123.456,78).

Infine, tenere presente che se si usano numeri in virgola mobile (float/double), questi possono rappresentare numeri molto grandi o molto piccoli, ma possono implicare un arrotondamento, anche per numeri interi. Avete mai sentito parlare di un programma che salvava numeri di telefono in virgola mobile? Per quanto i numeri salvati fossero numericamente vicini agli originali, gli effetti risultarono nefasti.

Date

Abbiate sempre timore dell’inserimento di una data. Molti sono i formati che un utente può inserire, aspettandosi che vengano correttamente interpretati. I due casi più critici sono i due formati ‘gg/mm/aaaa’ e ‘mm/gg/aaaa’, soprattutto perché alcune date possono risultare ambigue (es. 1/2/2018 è il primo febbraio o il 2 gennaio?).

Un modo per distinguerli è quello di controllare la cultura impostata nel browser. Su questo vale la pena citare una problematica specifica di MVC: a seconda del tipo di form, la data potrebbe essere analizzata secondo la cultura dell’utente o meno. Senza entrare nel dettaglio, questo dipende dal concetto che un’url con un parametro di tipo data deve rappresentare la stessa risorsa indipendentemente dalla cultura dell’utente. Trovate un buon approfondimento in questo articolo.

Il problema date viene tipicamente mitigato dall’uso di un datepicker, che permettendo all’utente di selezionare una data da un calendario, rimuove ambiguità e possibilità di confusione. Solitamente però, anche quando è impiegato un datepicker, viene lasciata la possibilità di inserire la data manualmente, rendendo necessari i controlli di cui sopra.

Upload di file

Per alcuni tipi di applicazioni, è necessario permettere all’utente di caricare file sul server, come immagini profilo, documenti, o altri file da processare. Per questi casi possono emergere altre problematiche da gestire.

Problemi di dimensioni

Caricando file, diventa verosimile che la richiesta di risorse diventi più elevata: la banda di rete e la velocità di scrittura su disco del server possono venir stressate notevolmente. Sarebbe anche desiderabile che gli utenti non abusino dello spazio messo a disposizione. La dimensione massima di un file da caricare deve essere per questo scelta attentamente – dimensioni troppo piccole possono limitare l’uso dell’applicazione, ma con dimensioni troppo grandi potremmo avere problemi di carico. Per impostare correttamente questa dimensione, controllare sia la configurazione dell’applicazione che quella del server web. Se più file possono essere caricati contemporaneamente, pensare anche ad un limite per la somma delle loro dimensioni. Controllare queste limitazioni anche lato client: sarebbe frustrante permettere il caricamento di un file solo per essere avvisati, al 90% dell’upload, che il file caricato è troppo grande.

Per form con file, tollerare problemi di timeout o di altri errori nel caricamento. Se un upload non va a buon fine, sarebbe utile riuscire almeno a salvare gli altri campi sottomessi dall’utente, o almeno a riproporli nel form.

Formato file

Spesso i file caricati devono essere di un tipo specifico. Controllare l’estensione è un buon inizio, ma può non essere sufficiente: per alcuni casi più delicati può essere necessario validare il tipo di file lato server. Assicurarsi che i file caricati non possano causare problemi di sicurezza – file eseguibili o contenenti script dovrebbero essere evitati o almeno gestiti con molta cautela.

Se si permette l’upload di file XML che verranno processati dal server, vale la pena approfondire inoltre problemi connessi ad attacchi legati a questo formato file, entrati dal 2017 nella classifica OWASP dei 10 maggiori rischi delle applicazioni web.

Autenticazione sessioni

Solitamente quando permettiamo ad un utente di sottomettere dati che verranno salvati sul database, ci aspettiamo che questo sia correttamente autenticato, e che esista una qualche forma di sessione che persiste questa informazione tra le varie chiamate http. Questo apre una nuova serie di problematiche.

Fine sessione

Qualsiasi implementazione scegliate per autenticare l’utente, dopo un certo tempo la sessione scadrà e l’utente dovrà autenticarsi di nuovo per accedere al programma. Se questo accade durante la compilazione di un form potremmo avere un problema – se non possiamo più identificare l’utente, tipicamente non possiamo permettergli di mettere le sue sozze, oramai inautenticate mani, sul nostro database. Persistendo l’autenticazione a lungo, usando varie tecniche che permetto al programma di ‘ricordarsi’ dell’utente, questo problema è fortemente mitigato, ma non completamente risolto.

Cosa fare quindi se un utente non più autorizzato sottomette dati? Innanzitutto è buona norma redirezionarlo ad una pagina di login, evitando errori lato server o pagine di errore. Se il form è particolarmente lungo, l’ideale sarebbe di riportare l’utente al form dopo il login, riproponendo i dati che aveva inserito fino a quel punto.

Sessioni inconsistenti

Usare variabili di sessione è una pratica sconsigliata per molti motivi – il tempo per il quale il server mantiene attiva la sessione non è prevedibile, ed il server si fa carico di dati utente in memoria, pregiudicandone la scalabilità. In alcuni casi può succedere qualcosa di ancora peggiore: potreste trovare di dati in sessione inconsistenti con quelli che un utente sta sottomettendo.

Prendiamo un esempio:

Immaginiamo che nell’applicazione ci sia una funzionalità che permette di modificare un ordine, magari strutturata in più passi. Per tenere traccia di quale ordine l’utente stia modificando, immaginiamo che in sessione venga salvato l’Id dell’ordine. Il problema: se l’utente apre più tab contemporaneamente, ognuna relativa ad un ordine diverso, è possibile che le modifiche ad un ordine (che l’utente sta visualizzando in una certa pagina) vengano applicate ad un altro (il cui Id è stato salvato in sessione perché è l’ordine nell’ultimo tab aperto).
Un problema simile occorse tempo fa alla Ryanair, che gestì la questione con un certo savoir-faire.

La soluzione più efficace: implementare applicazioni stateless. Qualsiasi informazione che deve passare da una pagina all’altra può essere inserita in un campo nascosto di un form, e questo consente un’architettura più pulita ed un minore carico del server web. Inoltre l’uso di variabili di sessione, oltre a poter creare i problemi citati, rende più difficile la gestione del load-balancing e può creare problematiche difficilmente diagnosticabili. Per approfondire, questo articolo potrebbe risultare di interesse.

Validazione lato server

Bisogna ricordare che tutto ciò che proviene da un client deve essere considerato inaffidabile. Le validazioni Javascript potrebbero non essere eseguite se Javascript è disabilitato, ed un utente con conoscenze tecniche sufficienti potrebbe cambiare a piacimento il valore dei parametri inviati, anche in campi nascosti.

I controlli lato client sono utili per una validazione molto responsiva, ma devono essere inevitabilmente replicati anche lato server.

Form postati troppe volte

Supponiamo che un form invii dati in POST al server, per salvare informazioni sul DB o per effettuare una qualche altra operazione. Se l’utente carica di nuovo la pagina di destinazione, può rischiare di ripetere l’operazione; nel caso del salvataggio di un record esistente, potrebbe non essere un problema, ma per operazioni che creano nuovi record sul database la situazione potrebbe avere effetti indesiderati, duplicando i dati inseriti. Conseguenze peggiori possono avvenire per richieste che scatenano altri tipi di azioni, come un trasferimento di denaro.

Per prevenire questo problema, una soluzione può essere quella di redirezionare l’utente ad un’altra pagina, caricata in GET, una volta effettuata l’operazione. In questo modo, anche se l’utente aggiorna la pagina non rischia di sottomettere di nuovo il form. Inoltre l’url nella barra del browser contiene tutti i parametri necessari a visualizzare la pagina: se salvata tra i preferiti del browser e ricaricata in seguito, darà un risultato coerente.

Quando la validazione fallisce

Anche quando l’utente sbaglia nell’inserimento dei dati, è importante non essere punitivi nei suoi confronti ma dare un messaggio chiaro che descriva il problema, e spieghi come l’utente può rimediare. In quest’ottica, sarebbe bene preservare i dati inseriti, in modo che in caso di errore non debbano essere ricompilati, specialmente per form molto lunghi.

Per il salvataggio di dati che coinvolgono più record del nostro database: ricordarsi che per la coerenza dei dati, è solitamente necessario che vengano salvati o tutti i record o nessuno – assicurarsi di effettuare le modifiche in modo transazionale, evitando in ogni caso un salvataggio di dati parziale.

Sicurezza

In questa sezione, discuteremo di problematiche legate ad utenti che hanno conoscenze tecniche sufficienti a tentare operazioni dannose per il sistema, attentando alla sua sicurezza. Le problematiche a riguardo son innumerevoli; sotto riportiamo i punti secondo noi più rilevanti, particolarmente rilevanti nella sottomissione di dati.

Code injection

Si parla di code injection quando sottomettendo del testo, si fa in modo che questo venga eseguito come codice dall’applicazione o da terze parti. Grazie all’uso di framework come MVC ed Entity Framework, abbiamo sempre più meccanismi di validazione automatica che vengono in nostro aiuto. Tuttavia, ancora nel 2017, l’injection era in testa nella classifica di vulnerabilità OWASP.

Un problema è che i pattern da evitare per prevenire questo problema dipendono dal contesto: l’input fornito dall’utente farà parte di una query SQL? Di una pagina HTML, eventualmente contenente script, visualizzabile da altri utenti? Di un file XML che certifica una transazione? Per tutti questi casi occorrono diversi tipi di validazione.  Se non siete più che sicuri di essere al sicuro, vale la pena di approfondire il problema.

Un tipo di code injection particolarmente utilizzato è il cross-site scripting (XSS), che può causare una vulnerabilità quando una stringa non validata viene inserita in una pagina web; per evitare problemi occorre considerare diverse casistiche.

Cross-Site Request Forgery (CSRF)

L’identità di un utente, che implica la sua autorevolezza nel poter eseguire alcune operazioni, è spesso determinata da informazioni salvate in un cookie, il cui contenuto viene inviato in ogni richiesta indirizzata al server. Un sito di terze parti potrebbe tuttavia causare ingannevolmente la sottomissione di una richiesta da parte di un utente alla nostra applicazione, per esempio tramite un form nascosto.

Per ovviare a questo problema, occorre inserire in ogni form che implichi un’azione un token creato appositamente per quel form, detto anti-forgery token; questo token sarà controllato al momento della sottomissione per assicurarsi che la richiesta provenga proprio da quel form, generato legittimamente dal nostro programma. In MVC, il framework mette a disposizione diversi metodi per la gestione di questo controllo secondo tutti i crismi di sicurezza.

HTTPS

I dati inviati tramite HTTP possono essere intercettati e letti; per ovviare a questo problema, assicurarsi di utilizzare HTTPS, almeno per quanto riguarda l’inserimento di password o dati particolarmente sensibili.

Prevenzione sottomissione automatica

Ci sono molti contesti in cui è sconveniente permettere l‘invio di un grande numero di richieste in modo automatico. Un caso classico è il login di un utente: vogliamo prevenire un grande numero di tentativi di accesso, per tutelare l’utente da accessi indesiderati. Se la password dell’utente è debole, un attaccante potrebbe indovinarla con poche migliaia di tentativi.

Più in generale, possiamo pensare a molte casistiche dove l’invio di richieste massive rappresenta un problema, come per la registrazione di account. Per risolvere il problema, si può garantire che una richiesta provenga da una persona e non da un programma automatizzato utilizzando un captcha, magari utilizzato all’ennesima richiesta dello stesso utente o per la stessa risorsa.

La checklist

Per concludere, riportiamo una checklist riassuntiva di tutte le problematiche discusse, per un consentire un rapido controllo dei form della vostra applicazione; speriamo che questo consentirà alle vostre applicazioni di resistere al meglio ai dati che verranno loro inviati dai vostri utenti più malvagi.

  1. Stringhe
    1. La lunghezza è correttamente limitata?
    2. Gli spazi iniziali e finali vengono rimossi dove appropriato?
    3. I campi testo con soli spazi/tab sono considerati vuoti?
    4. Codifica: è supportato l’UTF8 in tutta l’applicazione?
    5. Sono controllati pattern specifici per numeri telefono, email, ecc.?
  2. Numeri
    1. Vengono controllati i limiti inferiori e superiori per ogni numero?
    2. Decimali: sono permessi? I vari formati sono supportati?
    3. Decimali: sono gestiti eventuali problemi di arrotondamento?
  3. Date
    1. Sono supportati tutti i formati senza ambiguità?
  4. File upload
    1. Sono impostati limiti coerenti con i requisiti dell’applicazione per le dimensioni del file?
    2. L’applicazione reagisce al meglio in caso di timeout?
    3. Il formato del file è quello atteso?
  5. Autenticazione e sessioni
    1. In caso di fine sessione, il form viene gestito al meglio?
    2. L’applicazione è stateless? Altrimenti, vengono evitati problemi di sessioni inconsistenti?
  6. Validazione lato server
    1. Il server valida tutto l’input inviato come se non ci fosse una validazione lato client?
  7. Form postati troppe volte
    1. Vengono prevenute operazioni doppie in caso di reinvio dei dati del form?
  8. Validazione fallita
    1. In caso di errore, questo viene correttamente riportato all’utente?
    2. I dati inseriti vengono preservati nel form, se non sottomessi correttamente al primo tentativo?
    3. Modifiche che implicano cambiamenti su più record del DB vengono fatte in modo transazionale?
  9. Sicurezza
    1. L’applicazione evita correttamente il code injection?
    2. L’applicazione utilizza degli anti forgery token?
    3. Viene utilizzato HTTPS per la trasmissione di dati sensibili?
    4. Vengono evitate richieste massive automatizzate dove necessario?

 

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *