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

AuswirkungDetails
VertraulichkeitBereich: Vertraulichkeit

Anwendungsdaten lesen - XSS erlaubt Angreifern, Session-Cookies, Anmeldedaten und sensible Daten zu stehlen.
IntegritätBereich: Integrität

Anwendungsdaten ändern - Angreifer können Seiteninhalt modifizieren, Formulare einschleusen oder Anwendungsverhalten ändern.
ZugriffskontrolleBereich: 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="javas&#99;ript: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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#x27;');
    },
    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

  1. MITRE Corporation. "CWE-692: Incomplete Denylist to Cross-Site Scripting." https://cwe.mitre.org/data/definitions/692.html
  2. OWASP. "XSS Prevention Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  3. OWASP. "Java Encoder Project." https://owasp.org/owasp-java-encoder/