La Stored XSS, o Persistent XSS, è una vulnerabilità di sicurezza delle applicazioni web che si verifica quando l’applicazione memorizza l’input fornito dall’utente e in seguito lo incorpora in pagine web fornite ad altri utenti senza un’adeguata sanificazione o escape. Esempi includono post di forum web, recensioni di prodotti, commenti degli utenti e altri archivi di dati. In altre parole, stored XSS si verifica quando l’input dell’utente viene salvato in un archivio di dati e in seguito incluso nelle pagine web fornite ad altri utenti senza un’adeguata sanitizzazione.
La Stored XSS inizia con un aggressore che inietta uno script dannoso in un campo di input di un’applicazione web vulnerabile. La vulnerabilità potrebbe risiedere nel modo in cui l’applicazione web elabora i dati nella casella dei commenti, nel post del forum o nella sezione delle informazioni del profilo. Quando altri utenti accedono a questo contenuto archiviato, lo script dannoso iniettato viene eseguito nei loro browser. Lo script può eseguire un’ampia gamma di azioni, dal furto di cookie di sessione all’esecuzione di azioni per conto dell’utente senza il suo consenso.
PHP
Il codice sottostante presenta molteplici vulnerabilità. Fa due cose:
- Legge un commento dell’utente e lo salva nella variabile $comment.
- Aggiunge $comment alla colonna comment nella tabella comments in un database.
- In seguito, scorre tutte le righe nella colonna comment e le visualizza sullo schermo.
// Storing user comment
$comment = $_POST['comment'];
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
echo $row['comment'];
}Codice sanitizzato:
// Storing user comment
$comment = mysqli_real_escape_string($conn, $_POST['comment']);
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
$sanitizedComment = htmlspecialchars($row['comment']);
echo $sanitizedComment;
}Prima di visualizzare ogni commento sullo schermo, lo passiamo attraverso la funzione htmlspecialchars() per garantire che tutti i caratteri speciali siano convertiti in entità HTML. Di conseguenza, qualsiasi tentativo di XSS memorizzato non arriverà al browser dell’utente finale.
JavaScript (Node.js)
Il seguente codice JavaScript legge un commento ricevuto da un utente che è stato salvato in una tabella del database.
app.get('/comments', (req, res) => {
let html = '<ul>';
for (const comment of comments) {
html += `<li>${comment}</li>`;
}
html += '</ul>';
res.send(html);
});Il problema principale nel codice sopra è che legge l’input dell’utente salvato nel commento (dall’array dei commenti) e viene visualizzato come parte del codice HTML. Di conseguenza, quando un altro utente visualizza il commento di questo utente come HTML, il browser eseguirà tutti gli script iniettati in esso.
Codice sanitizzato:
const sanitizeHtml = require('sanitize-html');
app.get('/comments', (req, res) => {
let html = '<ul>';
for (const comment of comments) {
const sanitizedComment = sanitizeHtml(comment);
html += `<li>${sanitizedComment}</li>`;
}
html += '</ul>';
res.send(html);
});Parte della soluzione è sanificare l’HTML prima di mostrarlo all’utente. Possiamo rimuovere gli elementi HTML al di fuori della allowlist usando la funzione sanitizeHTML(). In generale, ci aspettiamo di consentire la formattazione di testo di base come grassetto e corsivo (b e i), ma rimuoveremmo elementi potenzialmente pericolosi o non sicuri come script e onload. Ulteriori informazioni sono disponibili sulla sua pagina ufficiale.
Python (Flask)
from flask import Flask, request, render_template_string
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])
def add_comment():
comment_content = request.form['comment']
comment = Comment(content=comment_content)
db.session.add(comment)
db.session.commit()
return 'Comment added!'
@app.route('/comments')
def show_comments():
comments = Comment.query.all()
return render_template_string(''.join(['<div>' + c.content + '</div>' for c in comments]))Il primo problema è che comment_content è impostato sull’invio del modulo dell’utente recuperato da request.form[‘comment’] senza sanificazione. Questo di per sé getta le basi per XSS archiviato e iniezione SQL. Inoltre, quando un utente desidera visualizzare i commenti, questi vengono visualizzati senza escape, un’altra ricetta perfetta per XSS archiviato.
Codice Sanitizzato:
from flask import Flask, request, render_template_string, escape
from flask_sqlalchemy import SQLAlchemy
from markupsafe import escape
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])
def add_comment():
comment_content = request.form['comment']
comment = Comment(content=comment_content)
db.session.add(comment)
db.session.commit()
return 'Comment added!'
@app.route('/comments')
def show_comments():
comments = Comment.query.all()
sanitized_comments = [escape(c.content) for c in comments]
return render_template_string(''.join(['<div>' + comment + '</div>' for comment in sanitized_comments]))Ci preoccupiamo di correggere le vulnerabilità XSS archiviate. Dobbiamo assicurarci che nessuno script dannoso venga salvato nel database; inoltre, faremo escape di qualsiasi contenuto prima di visualizzarlo come HTML.
Abbiamo utilizzato la funzione escape() per garantire che tutti i caratteri speciali nel commento inviato dall’utente vengano sostituiti con entità HTML. Come ci si aspetterebbe, i caratteri &, <, >, ’ e ” vengono convertiti in entità HTML (&, <, >, ’ e ”). Abbiamo apportato due modifiche: Sebbene la richiesta di input inviata dall’utente.form[‘comment’] venga salvata alla lettera, il contenuto di ogni commento salvato c passa attraverso la funzione escape() prima di essere inviato al browser dell’utente per essere visualizzato come HTML.
C# (ASP.NET)
public void SaveComment(string userComment)
{
var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES ('" + userComment + "')", connection);
// Execute the command
}
public void DisplayComments()
{
var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
while (reader.Read())
{
Response.Write(reader["Comment"].ToString());
}
// Execute the command
}Una delle vulnerabilità che osserviamo nel codice sopra è lo stored XSS. Il sistema memorizza qualsiasi commento l’utente inserisca senza alcuna modifica e in seguito lo mostra ad altri utenti.
Codice sanitizzato:
using System.Web;
public void SaveComment(string userComment)
{
var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES (@comment)", connection);
command.Parameters.AddWithValue("@comment", userComment);
}
public void DisplayComments()
{
var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
while (reader.Read())
{
var comment = reader["Comment"].ToString();
var sanitizedComment = HttpUtility.HtmlEncode(comment);
Response.Write(sanitizedComment);
}
reader.Close();
}Con alcune modifiche, la sicurezza del codice è migliorata. Stored-XSS è stato risolto utilizzando il metodo HttpUtility.HtmlEncode() prima di visualizzare userComment come parte di una pagina web. (Se sei curioso, la vulnerabilità di iniezione SQL è stata risolta utilizzando query SQL parametrizzate con valori passati separatamente anziché creare la query SQL tramite concatenazione di stringhe. Ciò può essere ottenuto utilizzando il metodo Parameters.AddWithValue() negli oggetti SqlCommand.