November 2, 2026
Understanding CWE-184: Incomplete List of Disallowed Inputs
Learn how CWE-184 lets attackers slip past a filter by finding the one dangerous value the developer forgot to list, from file extension…

By Walter Moar
4 min read
Learn how CWE-184 lets attackers slip past a filter by finding the one dangerous value the developer forgot to list, from file extension checks to profanity filters.
A blocklist tries to secure an application by naming the bad things and rejecting them: dangerous file extensions, forbidden words, disallowed characters. It is easy to write, since the developer can point directly at what they're worried about. The problem is that a blocklist is only as good as the list itself, and the set of things that could go wrong is usually much larger, much more varied, or much more easily disguised than whatever made it into the list.
CWE-184 Overview
The Common Weakness Enumeration (CWE) is a list of software and hardware security weaknesses. The CWEs are numbered, and each one describes a specific type of mistake that could lead to a security vulnerability. Understanding these common weaknesses helps identify potential security problems, both for the developer and an attacker.
CWE-184 occurs when protection relies on a list of disallowed values, but the list doesn't contain all the possible dangerous values. The flaw isn't in how the list is checked, but that the list is incomplete. New dangerous values can appear after the list was written, similar values can be used in a way the list didn't anticipate, or the developer just didn't think of everything an attacker would.
Blocklists fail in a few classic ways, but in nearly every case the more durable solution is an allowlist that defines the narrow set of things that are permitted. Everything else is rejected.
Example 1: Blocked File Extensions for Upload
An upload endpoint rejects executable file types by checking the extension against a list:
BLOCKED_EXTENSIONS = {'.bat', '.exe', '.php', '.sh'}
@app.route('/upload', methods=['POST'])
def upload():
f = request.files['file']
ext = os.path.splitext(f.filename)[1].lower()
if ext in BLOCKED_EXTENSIONS:
return 'File type not allowed', 400
f.save(os.path.join(UPLOAD_DIR, f.filename))
return 'Uploaded'BLOCKED_EXTENSIONS = {'.bat', '.exe', '.php', '.sh'}
@app.route('/upload', methods=['POST'])
def upload():
f = request.files['file']
ext = os.path.splitext(f.filename)[1].lower()
if ext in BLOCKED_EXTENSIONS:
return 'File type not allowed', 400
f.save(os.path.join(UPLOAD_DIR, f.filename))
return 'Uploaded'This blocks the obvious scripting extensions, but there are far more ways to get executable content onto the server than this list anticipates:
- Extensions outside the list:
.phtml,.php5,.pht,.cgi, and.aspare all extensions on various stacks that a list built around.phpalone would miss entirely - Case variation:
.PHPor.Phpbypasses a case-sensitive comparison, depending on how the check and the underlying filesystem each handle case - Double extensions: A file named
shell.php.jpgpasses this specific check if the server is misconfigured to execute based on any recognized extension in the name rather than only the last one
Each of these is a value the list's author didn't write down, either because it wasn't known at the time, because it's a variant of something that was blocked, or because it depends on server behavior the developer wasn't thinking about when writing the filter. This is the core problem with CWE-184: the list reflects what the author anticipated, and an attacker's job is specifically to find what they didn't.
Example 2: Blocked Keywords for SQL Injection
A search feature tries to stop SQL injection by rejecting input containing SQL keywords:
BLOCKED_WORDS = ['DROP', 'INSERT', 'SELECT', 'UNION', '--']
def search(query):
upper_query = query.upper()
if any(word in upper_query for word in BLOCKED_WORDS):
return 'Invalid search term', 400
cursor.execute(f"SELECT * FROM products WHERE name LIKE '%{query}%'")BLOCKED_WORDS = ['DROP', 'INSERT', 'SELECT', 'UNION', '--']
def search(query):
upper_query = query.upper()
if any(word in upper_query for word in BLOCKED_WORDS):
return 'Invalid search term', 400
cursor.execute(f"SELECT * FROM products WHERE name LIKE '%{query}%'")This blocks the most common SQL injection keywords, but SQL has a huge number of keywords, plus the filter is checking for exact substrings:
- Case and whitespace tricks: the
.upper()call already handles case here, but comments like /**/ can be used with in a keyword, such as inINS/**/ERTorSEL/**/ECT - Equivalent operators: if
UNIONis disallowed, boolean-based blind injection usingOR 1=1or time-based injection usingSLEEP()accomplishes the same thing - Encoding: A URL-encoded or double-encoded payload may not match the blocklist if the check runs before decoding happens, similar in spirit to the decode-order issue in path traversal.
The issue is that this filter is trying to list "the ways to do SQL injection," so it is trying to find all the dangerous things in SQL. Parameterized queries are the well-known solution here, but blocklists (or allowlists that are too permissive) are still common.
Example 3: Blocked Characters in a Profanity Filter
A comment system censors offensive words by checking submitted text against a list of banned terms:
const BANNED_WORDS = ['badword1', 'badword2', 'badword3']
function filterComment(text) {
const lower = text.toLowerCase()
const containsBanned = BANNED_WORDS.some(word => lower.includes(word))
return containsBanned ? null : text
}const BANNED_WORDS = ['badword1', 'badword2', 'badword3']
function filterComment(text) {
const lower = text.toLowerCase()
const containsBanned = BANNED_WORDS.some(word => lower.includes(word))
return containsBanned ? null : text
}This is a less dangerous example than the first two, but it shows the same weakness in a form that's easy to visualize:
- Character substitution (leetspeak):
b4dw0rd1orb a d w o r d 1with spaces inserted won't match a plain substring check - Homoglyphs: Visually similar Unicode characters from other alphabets (like a Cyrillic
аinstead of a Latina) look nearly identical to a human reader but fail a string match - New terms entirely: language is always changing, and new bad words are always being added. A list from one point in time will soon be out of date
This example shows that CWE-184 isn't only a "sophisticated hacker" problem. Anywhere a list is used to match user input, the user will always try to find a way around that list.
What to Remember
The most important points to keep in mind about incomplete blocklists are:
- A blocklist can be implemented with perfectly correct comparison logic and still be a weakness, because the weakness is in what the list contains, not how it's checked
- Blocklists fail through unlisted equivalents (new extensions, new keywords, new slang), alternate representations of listed values (case, encoding, substitution, homoglyphs), and simple incompleteness
- Defeating a blocklist usually happens by finding a dangerous value that the list doesn't contain (not by breaking the list's matching logic)
- An allowlist containing what's permitted, and rejecting everything else, is usually more robust than trying to find everything that's forbidden. Allowlists tend to be smaller and more stable
- Even when an allowlist is possible, perhaps a different mechanism (parameterized query instead of keyword filtering, content-type detection instead of extension filtering) is a better solution
Want to learn more about security weaknesses? I'm working through the CWE list and doing writeups for security challenges. Follow along for more articles like this one.