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:
- CVE-2007-5727: the denylist only removes the SCRIPT tag.
- CVE-2006-3617: same, the denylist only removes the SCRIPT tag.
- CVE-2006-4308: the denylist only checks the "javascript:" scheme.
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.
- Encode on output for the exact context: HTML body, attribute, URL or script.
- Use a template engine that escapes by default and avoid the escape hatches.
- Where users must submit HTML, use a maintained sanitizer built on an allowlist of tags and attributes.
- 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.