Every developer has written an escaping function at some point, usually in a hurry, usually to fix a bug that already made it to production. The instinct is to treat sanitization as one problem with one solution. It isn't. Escaping a string for safe use in HTML is a completely different operation from escaping it for SQL, and both are different again from escaping it for a shell command.
The mistake we see most often in code reviews is a single sanitize() or clean() helper that gets reused everywhere, regardless of where the string ends up. That function might strip a few HTML tags, quietly assume it's "safe enough," and get passed straight into a database query or a shell call. The bug doesn't show up in testing. It shows up when a user types an apostrophe, a semicolon, or a backtick into a form field that nobody thought to attack.
Why context is the whole problem
Sanitization and escaping are not the same operation, even though people use the words interchangeably. Sanitizing removes or rewrites content you consider dangerous. Escaping transforms characters so they are interpreted as literal data, not as syntax, by whatever parser reads them next. The parser is the part that changes with context, and that's why a single generic escaping function is a trap.

Photo by RDNE Stock project on Pexels
An HTML parser cares about <, >, &, and quote characters because those trigger tag and attribute parsing. A SQL parser cares about quote characters and backslashes because those close string literals early. A shell interpreter cares about spaces, quotes, backticks, dollar signs, and semicolons because those are all command-separator or substitution syntax. None of these character sets fully overlap, and none of the escaping rules transfer cleanly between them.
This matters more than it sounds like it should, because copy-pasted "sanitize this input" snippets circulate constantly between projects. A function written for one context, dropped into another, often looks like it works right up until someone tests it with the one character that breaks its assumptions.
Escaping for HTML output
The HTML case is the one most developers get right by default, mostly because templating engines handle it automatically now. React escapes interpolated values before rendering. Jinja2 and Django templates auto-escape unless you explicitly mark a string as safe. The danger shows up when someone bypasses that default, usually with dangerouslySetInnerHTML, {% autoescape false %}, or a raw string concatenation into an HTML response.
The rule for HTML escaping is narrow and mechanical: convert &, <, >, ", and ' into their entity equivalents before the string touches the page. If you're building an attribute value rather than element content, quote the attribute and escape the quote character you used. The OWASP cheat sheet series has a specific entry on output encoding that walks through the different HTML contexts, element body, attribute, URL, JavaScript block, because each one technically needs slightly different treatment.
function escapeHtml(str) {
return str.replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
}[c]));
}
That snippet is fine for element content. It is not fine for a string you're about to drop inside a <script> block or a style attribute, both of which have their own escaping requirements that plain HTML entity encoding doesn't cover. If you find yourself building a script tag's contents from user input at all, that's usually a sign to redesign the feature rather than escape harder.
Escaping for SQL
SQL is where the "just escape the quotes" instinct causes the most damage, because it's almost right, and almost right is worse than obviously wrong. Manually escaping single quotes by doubling them, or backslash-escaping them, works for the simplest injection attempts and fails against encoding tricks, multi-byte character sets, and second-order injection, where a value gets stored once and reused unsanitized later.
The actual fix is not a better escaping function. It's parameterized queries, sometimes called prepared statements. The database driver keeps your query structure and your data completely separate, so there's no string concatenation step for an attacker to exploit in the first place.
# Don't do this
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
# Do this
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Nearly every mainstream database library supports parameterized queries, from psycopg2 in Python to prepared statements in Node's pg driver to PDO in PHP. The Wikipedia entry on SQL injection is worth skimming even for experienced developers, because it documents attack variations that manual escaping consistently misses, including ones that don't rely on quote characters at all. Manual escaping is a fallback for the rare case where parameterization genuinely isn't available, such as dynamically building identifier names, not a general-purpose defense.
Escaping for shell commands
Shell escaping is the context developers forget about most, probably because it comes up less often than HTML or SQL. It's also the least forgiving. A single unescaped space or semicolon in a filename or user-supplied argument can turn one command into two, or redirect output somewhere you didn't intend.
The safest pattern is to avoid the shell entirely when you can. Most languages offer a way to call a subprocess with an argument list instead of a single command string, which sidesteps shell parsing altogether.
import subprocess
# Risky: shell interprets the whole string
subprocess.run(f"convert {filename} output.png", shell=True)
# Safer: arguments passed directly, no shell parsing
subprocess.run(["convert", filename, "output.png"])

Photo by Yan Krukau on Pexels
When you genuinely need shell interpretation, Python's shlex.quote and equivalent functions in other languages wrap a string in single quotes and escape any embedded single quotes correctly, which is much harder to get right by hand than it looks. Bash's own manual, documented in the GNU Bash reference, spells out exactly which characters are special and why manual quoting attempts so often miss an edge case like a trailing backslash or a newline inside the string. Node's Node.js documentation covers the same problem from the child_process side, and the advice lands in the same place: pass arguments as an array whenever the API allows it.
A rule of thumb for picking the right function
"The question I ask in every code review isn't whether a string got escaped. It's what parser reads that string next, because that's the only thing that tells you which escaping rule actually applies." - Dennis Traina, founder of 137Foundry
That question is a useful gut check for any team working across full-stack web development projects, where the same string can pass through a template, a database call, and occasionally a shell command in the same request lifecycle. Treat each boundary as a separate decision, not one global sanitization pass at the edge of your application.

Photo by Monstera Production on Pexels
Building this into your workflow, not just your memory
Escaping rules are easy to know and easy to forget under deadline pressure, which is why the best fix is structural rather than educational. Use parameterized queries as the default and make raw SQL string building a flagged exception in code review. Rely on your templating engine's auto-escaping instead of hand-rolling HTML encoding, and treat any dangerouslySetInnerHTML or mark_safe call as something that needs a second reviewer.
For shell commands, default to argument lists over shell strings, and reach for a vetted quoting library the moment shell interpretation is unavoidable. None of these are exotic practices. They're just easy to skip when a feature needs to ship by Friday, which is exactly when the shortcut gets written into the codebase permanently and quietly outlives the person who wrote it.

Photo by K on Pexels
Data pipelines make this worse rather than better. A CSV import that lands in your database, gets rendered on a dashboard, and occasionally triggers a shell-based export script needs three separate escaping strategies applied consistently, not one shared utility function everyone assumes covers all three. If your team is working through a data integration project that touches a lot of user-supplied input across multiple systems, mapping out every boundary where a string crosses from one context into another is worth doing before the pipeline goes live, not after the first strange bug report.
Testing the boundaries, not just the happy path
Most teams test escaping logic with the inputs they expect: normal names, normal emails, normal search terms. The inputs that actually break things are the ones nobody typed in manually, things like a name containing an apostrophe, a search term containing a semicolon, or a filename containing a backtick. Building a small fixture list of these "hostile but plausible" strings and running it through every boundary in your test suite catches far more than a generic security scanner will, because it's testing your actual code paths instead of a generic pattern match.
It's also worth revisiting these tests whenever you swap a library or upgrade a framework major version. Escaping behavior is exactly the kind of thing that changes quietly between versions, and a test suite that encodes your assumptions explicitly will catch a regression long before a user does.
Where to go from here
Audit the places in your codebase where user input crosses a boundary: into HTML, into a query, into a shell call, into a file path. For each one, confirm you're using the context-appropriate mechanism, not a general-purpose "clean" function inherited from an old project. The MDN Web Docs are a solid reference for HTML and JavaScript-side encoding specifics, and most database driver documentation pages cover parameterization directly.
If you want a second set of eyes on how your application handles this across its stack, that's the kind of review our web development team does regularly, and it's usually a smaller fix than teams expect once the boundaries are mapped out clearly. Visit 137Foundry to see more of how we approach this kind of work.