Unvollständige Denylist für Cross-Site-Scripting
Beschreibung
Unvollständige Denylist für Cross-Site-Scripting ist eine zusammengesetzte Schwachstelle, bei der Software einen Denylist-basierten Schutzmechanismus verwendet, um XSS-Angriffe (Cross-Site Scripting) zu verhindern, aber die Denylist nicht alle möglichen XSS-Angriffsvektoren abdeckt. Denylists versuchen bekannte gefährliche Muster wie <script>-Tags oder javascript:-URLs zu filtern, aber Browser parsen Web-Inhalte auf höchst variable Weisen mit zahlreichen Encoding-Optionen und Tag-Variationen. Da keine Denylist alle Angriffsvariationen vorhersehen kann, ist dieser Ansatz für XSS-Prävention grundlegend fehlerhaft und erlaubt Angreifern, neuartige Payloads zu erstellen, die den unvollständigen Filter umgehen.
Risiko
Denylist-basierte XSS-Prävention erzeugt ein falsches Sicherheitsgefühl, während sie verwundbar bleibt. Angreifer erforschen aktiv neue Umgehungstechniken, einschließlich Zeichenencoding-Variationen, HTML-Parsing-Eigenheiten, Event-Handler, CSS-basierte Angriffe und browserspezifische Verhaltensweisen. Jede neue Browser-Funktion führt potenziell neue XSS-Vektoren ein, die Denylists nicht abdecken. Der Ansatz versagt besonders gegen: gemischte Groß-/Kleinschreibung (<ScRiPt>), alternative Encodings (UTF-7, HTML-Entities), weniger bekannte Event-Handler (onerror, onload) und neuartige HTML5-Funktionen. Dies erzeugt fortlaufende Verwundbarkeit, da Angreifer kontinuierlich Umgehungen finden, während Verteidiger Denylists nachträglich aktualisieren müssen.
Lösung
Verwenden Sie Allowlist-basierte Eingabevalidierung statt Denylists - definieren Sie genau, welche Zeichen und Muster erlaubt sind, und lehnen Sie alles andere ab. Wenden Sie kontextbewusstes Output-Encoding an, das für den Renderkontext der Daten angemessen ist (HTML-Body, Attribut, JavaScript, CSS, URL). Verwenden Sie etablierte Sicherheitsbibliotheken wie OWASP Java Encoder oder DOMPurify statt benutzerdefinierter Filterung. Implementieren Sie Content Security Policy (CSP) als Defense in Depth. Verwenden Sie Template-Engines mit automatischem Encoding. Verlassen Sie sich nie ausschließlich auf Eingabefilterung - codieren Sie immer die Ausgabe. Erwägen Sie die Verwendung strukturierter Datenformate und APIs, die kein HTML interpretieren.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Vertraulichkeit Anwendungsdaten lesen - XSS erlaubt Angreifern, Session-Cookies, Anmeldedaten und sensible Daten zu stehlen. |
| Integrität | Bereich: Integrität Anwendungsdaten ändern - Angreifer können Seiteninhalt modifizieren, Formulare einschleusen oder Anwendungsverhalten ändern. |
| Zugriffskontrolle | Bereich: Zugriffskontrolle Privilegien erlangen oder Identität annehmen - Session-Hijacking ermöglicht Angreifern, sich als legitime Benutzer auszugeben. |
Beispielcode
Anfälliger Code
<?php
// Anfällig: Unvollständige Denylist entfernt nur <script>-Tag
function vulnerable_sanitize_script_only($input) {
// Entfernt nur exakte <script>-Tags
$sanitized = preg_replace('/<script>/i', '', $input);
$sanitized = preg_replace('/<\/script>/i', '', $sanitized);
return $sanitized;
}
// Umgehungen:
// <SCRIPT>alert(1)</SCRIPT> - andere Groß-/Kleinschreibung (wenn nicht /i)
// <script >alert(1)</script> - Leerzeichen im Tag
// <scr<script>ipt>alert(1)</scr</script>ipt> - verschachtelte Tags
// <img onerror="alert(1)" src=x> - Event-Handler
// <body onload="alert(1)"> - andere Tags
// Anfällig: Entfernt häufige gefährliche Muster aber verpasst andere
function vulnerable_sanitize_incomplete($input) {
$denylist = array(
'<script', '</script>',
'javascript:',
'onclick', 'onerror', 'onload'
);
$sanitized = str_ireplace($denylist, '', $input);
return $sanitized;
}
// Umgehungen:
// <img src=x oOnError="alert(1)"> - gemischte Groß-/Kleinschreibung Event-Handler
// <svg onmouseover="alert(1)"> - nicht gelisteter Event-Handler
// <a href="javascript:alert(1)"> - HTML-Entities
// <script/src="evil.js"> - kein Leerzeichen nötig
// <IMG SRC=/ onerror="alert(1)"> - nicht gelistete src-Variante
?>
<!-- Anfällig: Ausgabe ohne korrektes Encoding -->
<div class="user-content">
<?php echo vulnerable_sanitize_incomplete($_GET['comment']); ?>
</div>
// Anfällig: JavaScript Denylist-Filter
function vulnerableSanitize(input) {
// Anfällig: Prüft nur auf häufige Muster
const denylist = [
/<script[\s\S]*?>[\s\S]*?<\/script>/gi,
/javascript:/gi,
/on\w+=/gi // Versucht Event-Handler zu fangen
];
let sanitized = input;
for (const pattern of denylist) {
sanitized = sanitized.replace(pattern, '');
}
return sanitized;
}
// Umgehungen:
// <img src="x" onerror ="alert(1)"> - Leerzeichen vor Gleichzeichen
// <svg><script>alert(1)</script></svg> - SVG-Kontext
// java\nscript:alert(1) - Zeilenumbruch in URL
// <iframe srcdoc="<script>alert(1)</script>"> - srcdoc-Attribut
// <math><mtext><table><mglyph><style><img src=x onerror=alert(1)></style></mglyph></table></mtext></math> - Parser-Verwirrung
// Anfällig: innerHTML mit "bereinigtem" Inhalt verwenden
function displayComment(comment) {
const sanitized = vulnerableSanitize(comment);
document.getElementById('comments').innerHTML += sanitized;
}
# Anfällig: Python Denylist-Ansatz
import re
def vulnerable_sanitize(user_input):
"""Unvollständige Denylist für XSS-Prävention."""
# Anfällig: Musterliste ist unvollständig
dangerous_patterns = [
r'<script.*?>.*?</script>',
r'javascript:',
r'on\w+=',
r'<iframe',
r'<object',
r'<embed'
]
sanitized = user_input
for pattern in dangerous_patterns:
sanitized = re.sub(pattern, '', sanitized, flags=re.IGNORECASE)
return sanitized
# Umgehungen beinhalten:
# <img src=x onerror=alert(1)> - img nicht blockiert
# <svg/onload=alert(1)> - svg nicht blockiert
# <body background="javascript:alert(1)"> - background-Attribut
# <input onfocus=alert(1) autofocus> - autofocus-Trick
# <marquee onstart=alert(1)> - marquee-Tag
# <details ontoggle=alert(1) open> - HTML5 details-Element
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/comment')
def show_comment():
comment = request.args.get('text', '')
sanitized = vulnerable_sanitize(comment)
# Anfällig: "bereinigten" Inhalt rendern
return render_template_string(f'<div>{sanitized}</div>')
// Anfällig: Java Servlet mit unvollständiger Filterung
import javax.servlet.http.*;
import java.io.*;
public class VulnerableCommentServlet extends HttpServlet {
private static final String[] DENYLIST = {
"<script", "</script>",
"javascript:",
"onerror=", "onclick=", "onload="
};
private String vulnerableSanitize(String input) {
String result = input;
for (String pattern : DENYLIST) {
result = result.replaceAll("(?i)" + pattern, "");
}
return result;
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
String comment = request.getParameter("comment");
String sanitized = vulnerableSanitize(comment);
response.setContentType("text/html");
PrintWriter out = response.getWriter();
// Anfällig: "bereinigten" Inhalt direkt schreiben
out.println("<html><body>");
out.println("<div class='comment'>" + sanitized + "</div>");
out.println("</body></html>");
}
}
Lösung
<?php
// Korrigiert: Korrektes Output-Encoding verwenden, keine Denylist
function secure_output_html($input) {
// Korrigiert: Für HTML-Kontext codieren
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
function secure_output_attribute($input) {
// Korrigiert: Für HTML-Attribut-Kontext codieren
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
function secure_output_javascript($input) {
// Korrigiert: Für JavaScript-String-Kontext codieren
return json_encode($input, JSON_HEX_TAG | JSON_HEX_APOS |
JSON_HEX_QUOT | JSON_HEX_AMP);
}
// Für Rich Text, etablierte Bibliothek verwenden
function secure_rich_text($input) {
// HTMLPurifier-Bibliothek verwenden
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,u,a[href],ul,ol,li');
$purifier = new HTMLPurifier($config);
return $purifier->purify($input);
}
?>
<!-- Korrigiert: Korrektes Encoding im Template -->
<div class="user-content">
<?php echo secure_output_html($_GET['comment']); ?>
</div>
<!-- Korrigiert: Attribut-Kontext -->
<input type="text" value="<?php echo secure_output_attribute($value); ?>">
<!-- Korrigiert: JavaScript-Kontext -->
<script>
var userInput = <?php echo secure_output_javascript($data); ?>;
</script>
// Korrigiert: Korrektes Encoding und sichere DOM-Manipulation verwenden
function secureTextContent(input) {
// Korrigiert: textContent verwenden, das kein HTML parst
const div = document.createElement('div');
div.textContent = input; // Automatisch escaped
return div.innerHTML; // Die escaped Version erhalten
}
// Korrigiert: DOM-Methoden statt innerHTML verwenden
function displayCommentSecure(comment) {
const container = document.getElementById('comments');
const commentDiv = document.createElement('div');
commentDiv.className = 'comment';
commentDiv.textContent = comment; // Sicher: parst kein HTML
container.appendChild(commentDiv);
}
// Korrigiert: Für Rich Text DOMPurify-Bibliothek verwenden
function displayRichComment(htmlContent) {
// DOMPurify für HTML-Bereinigung verwenden
const clean = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['b', 'i', 'u', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href']
});
document.getElementById('comments').innerHTML = clean;
}
// Korrigiert: Kontextbewusste Encoding-Funktion
const encode = {
forHTML: (str) => {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
},
forAttribute: (str) => {
return str.replace(/[^a-zA-Z0-9]/g, (char) => {
return '&#' + char.charCodeAt(0) + ';';
});
},
forJavaScript: (str) => {
return JSON.stringify(str);
}
};
# Korrigiert: Korrektes Encoding mit Flask/Jinja2 verwenden
from flask import Flask, request, render_template, Markup
from markupsafe import escape
import bleach
app = Flask(__name__)
# Korrigiert: Automatisches Escaping mit Jinja2
@app.route('/comment')
def show_comment():
comment = request.args.get('text', '')
# render_template escaped Variablen automatisch
return render_template('comment.html', comment=comment)
# comment.html Template:
# <div>{{ comment }}</div> <!-- Automatisch escaped -->
# Korrigiert: Für Rich Text Allowlist mit bleach verwenden
ALLOWED_TAGS = ['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li', 'br']
ALLOWED_ATTRS = {'a': ['href', 'title']}
def secure_rich_text(html_input):
"""HTML mit Allowlist-Ansatz bereinigen."""
return bleach.clean(
html_input,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRS,
strip=True
)
@app.route('/rich-comment')
def show_rich_comment():
comment = request.args.get('text', '')
# Mit Allowlist bereinigen, dann als sicher für Template markieren
clean_comment = secure_rich_text(comment)
return render_template('comment.html',
comment=Markup(clean_comment))
# Korrigiert: CSP-Header für Defense in Depth
@app.after_request
def add_security_headers(response):
response.headers['Content-Security-Policy'] = \
"default-src 'self'; script-src 'self'"
return response
// Korrigiert: Java mit OWASP Encoder
import org.owasp.encoder.Encode;
import javax.servlet.http.*;
import java.io.*;
public class SecureCommentServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
String comment = request.getParameter("comment");
response.setContentType("text/html; charset=UTF-8");
response.setHeader("Content-Security-Policy",
"default-src 'self'");
PrintWriter out = response.getWriter();
out.println("<html><body>");
// Korrigiert: OWASP Encoder für HTML-Kontext verwenden
out.print("<div class='comment'>");
out.print(Encode.forHtml(comment));
out.println("</div>");
// Für Attribute
out.print("<input value='");
out.print(Encode.forHtmlAttribute(comment));
out.println("'>");
// Für JavaScript
out.print("<script>var data = '");
out.print(Encode.forJavaScript(comment));
out.println("';</script>");
out.println("</body></html>");
}
}
CVE-Beispiele
- CVE-2007-5727: XSS-Filter-Umgehung - Denylist entfernte nur
<SCRIPT>-Tags, erlaubte andere Vektoren. - CVE-2006-3617: Webanwendungs-XSS-Filter blockierte nur
<SCRIPT>-Tags. - CVE-2006-4308: XSS-Filter pruft nur auf "javascript:"-Muster, erlaubte Variationen.
Referenzen
- MITRE Corporation. "CWE-692: Incomplete Denylist to Cross-Site Scripting." https://cwe.mitre.org/data/definitions/692.html
- OWASP. "XSS Prevention Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP. "Java Encoder Project." https://owasp.org/owasp-java-encoder/