Safeguard
Application Security•

CWE-692 Incomplete Denylist to XSS: Why Blocking script Tags Fails

Filtering out script tags does not stop cross-site scripting, because browsers parse HTML in too many ways. CWE-692 shows why denylists lose and what to use instead.

Safeguard Security Team
3 min read

CWE-692, Incomplete Denylist to Cross-Site Scripting, describes a product that defends against XSS with a denylist, a list of bad strings to strip or block, where the list is incomplete. XSS variants then walk straight through.

It affects web based products and web servers, and it is a very common first attempt at "fixing" XSS. MITRE's extended description explains the problem: web browsers vary so widely in how they parse pages that a denylist cannot keep track of all the variations. The record points to the well known XSS Cheat Sheet, which contains a large number of attacks aimed precisely at bypassing incomplete denylists.

How it happens

A developer sees an alert box from a script tag carrying an alert call and writes a filter that removes script. The bug is reported as fixed. The attacker then uses a different way to run code, and the filter has no opinion.

MITRE's observed examples are all this pattern:

Here is an illustration of a naive filter in Python:

# Vulnerable: removes one known-bad string and trusts the rest
import re
def clean(s):
    return re.sub(r"(?i)<script.*?>", "", s)

page = "<div>" + clean(user_input) + "</div>"

Any input that executes script without using a script tag is untouched, for example an image tag with an error handler attribute. Case tricks, nesting and encoding add more ways around it. This is a pattern illustration, not a catalog of bypasses.

What an attacker gains

MITRE lists execution of unauthorized code or commands, with confidentiality, integrity and availability all in scope. In a browser that means running script in the victim's session on your origin: reading what the page can read, acting as the user, and rewriting the page.

How to find it

The record has no detection methods listed, but the checks are straightforward. Search the codebase for sanitizing functions built on regular expressions or string replacement. Then test them with a range of XSS payloads rather than the one you remember, and with a dynamic scanner pointed at the running application.

How to fix it

No mitigation text appears in the record. The usual answer, which also follows from why denylists fail, is to stop deciding what is bad and instead make the output safe by construction.

  1. Encode on output for the exact context: HTML body, attribute, URL or script.
  2. Use a template engine that escapes by default and avoid the escape hatches.
  3. Where users must submit HTML, use a maintained sanitizer built on an allowlist of tags and attributes.
  4. Add a Content Security Policy as a second layer, not a first.
# Fixed: encode for the HTML context instead of filtering
from markupsafe import escape
page = "<div>" + str(escape(user_input)) + "</div>"

Try it yourself

Click through it at your own pace, or open it full screen.

Quick check

Why can a denylist not be completed?

Because browsers parse HTML in many different ways, so the set of strings that can run script is too large and varied to enumerate.

What replaces the denylist?

Context-aware output encoding, escaping templates, and an allowlist sanitizer where HTML must be accepted.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.