Input Validation and Output Encoding
Validate on the way in, encode on the way out, and injection mostly dies.
What you will learn
- -Write allowlist validation instead of denylist filters
- -Kill SQL injection with parameterized queries, not string cleaning
- -Encode output for the exact context it lands in
The single quote that breaks a SQL query and the <script> that pops an are the same failure. In both cases your app took data and let it run as code. Two defenses stop nearly all of it, and developers reach for the wrong one constantly, so the split straight before you write another line.
Validation asks one question: is this input shaped like what I expect? asks a different one: how do I safely place this data into a specific output? You need both, and neither substitutes for the other.
Validate with an allowlist
Validation happens at the boundary, the moment input arrives, and its job is to reject anything that does not match a known-good shape. A US zip code is five digits. Match ^\d{5}$ and drop everything else. A username is letters, numbers, and underscores up to 32 characters. Define what good looks like and refuse the .
Do this with an allowlist, never a denylist. A denylist tries to enumerate bad input: block ', block <script>, block ../. You will lose, every time, because attackers have more encodings than you have filters. Uppercase, -encoding, unicode homoglyphs, null bytes, nested tags that survive a single strip pass. An allowlist flips the burden: instead of naming every attack, you name the handful of things you accept.
The that defeat weak filters, like %2e%2e%2f or <scr<script>ipt>, are worth understanding so you can see why denylists fail. Fire them only at applications you own or are cleared to test. Throwing bypass at someone else's live site to see what sticks is an attack, not research.
Validation does not stop injection
Here is the part people get wrong. Cleaning input does not stop . The real fix is to keep data out of the code channel entirely, so the database never parses your value as SQL in the first place. That is what parameterized queries do.
# vulnerable: email is concatenated into the query text
cur.execute("SELECT * FROM users WHERE email = '" + email + "'")
# safe: email is bound as a value, never parsed as SQL
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
The placeholder version sends the query and the data on separate rails. The database treats email as a string value no matter what characters it holds, so a single quote is just a character. Same idea everywhere a "code plus data" exists: pass an argv array to execve instead of building a string, use prepared statements, use a template engine that escapes by default. Validation is a useful second layer. It is not the fix.
Encode for the exact context
Cross-site scripting is the output-side twin of injection. When you drop untrusted data into a page, you have to encode it for the precise spot it lands in, because each context has different dangerous characters.
- body: entity-encode, so
<becomes<and the browser renders it as text. - HTML attribute: entity-encode and keep the value quoted, or an attacker closes the attribute and adds their own.
- Inside a
<script>block or anonclick: -encode, and honestly, just do not put user data there. That context is a minefield. - URL : percent-encode.
Modern encode HTML output by default. React escapes {value} for you. The bugs happen when you step around the framework: dangerouslySetInnerHTML, a Jinja |safe filter, innerHTML =. Those are the lines to hunt.
$grep -rn 'dangerouslySetInnerHTML' src/$grep -rn '|safe' templates/$grep -rn 'innerHTML' src/Your login endpoint concatenates the username into a SQL string. What actually fixes the injection?
You are inserting an untrusted comment into an HTML page body. Name the one character whose encoding matters most to stop it from starting a tag.
A script tag has to start somehow. Encode the character that opens any HTML tag and the browser prints it as text.