injection-flaws
Skill2 starsChanged 49 days agoMITView on GitHub
Detects injection vulnerabilities in code — SQL/NoSQL injection (including NoSQL operator injection), OS command injection, cross-site scripting (XSS), path traversal, SSRF, insecure deserialization, template injection (SSTI), XXE, prototype pollution, ReDoS, open redirect, and unsafe file upload handling. Covers Node/JS, Python, Java, Go, PHP, Ruby, C# with per-language grep patterns and context-review rules. Use when auditing input handling, query construction, file operations, subprocess calls, XML parsing, file uploads, or template rendering.
In this repository23 other files it gives its agents.
- android-code-securitySkill
- auth-reviewSkill
- config-hardeningSkill
- container-iac-securitySkill
- course-platform-securitySkill
- crypto-reviewSkill
- cve-researchSkill
- data-exposureSkill
- dependency-vulnsSkill
- design-threat-reviewSkill
- django-securitySkill
- dotnet-securitySkill
- flow-securitySkill
- flutter-securitySkill
- graphql-securitySkill
- laravel-securitySkill
- llm-securitySkill
- mobile-securitySkill
- rails-securitySkill
- secrets-detectionSkill
- security-auditSkill
- skill-forgeSkill
- spring-securitySkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 106
Egress controls (defense verification, A10): when the app fetches user-influenced URLs, also check the environment makes SSRF expensive:
- Distinctive: line 109
No egress proxy/allowlist at all → mention as hardening: the fix lives in network config, not app code
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Injection Flaws
Method
For each category: (1) grep for sink patterns — the table below and references/patterns.md have ready-to-run commands per language, and references/frameworks.md has framework-specific dangerous/safe construct tables (Express, Django, Spring, Rails, Laravel, Go); (2) for each hit, trace backwards to the input source; (3) report only if untrusted input reaches the sink without a real sanitizer.
Source → sink tracing. Untrusted sources: HTTP params/body/headers/cookies, file uploads, DB rows fed to templates, webhook payloads, environment-adjacent user data, message queues. Sanitizers that count: parameterized queries, allowlist validation, DOMPurify for HTML, framework auto-escaping (verify it's actually on), shlex.quote/argument-array subprocess calls, canonicalized + allowlisted paths. "Escaping" done by string formatting does NOT count.
Second-order (stored) flows. The input isn't always the request in front of you. Treat these as untrusted sources and re-check sink greps against their outputs:
- DB/model rows at render time — the classic stored XSS: sanitized on write, rendered raw on read (
<%- user.bio %>,\|safe,{!! !!},v-html,dangerouslySetInnerHTMLon model fields). Write-path validation ≠ read-path safety; check the READ path even when the write path looks clean. - Webhook/queue/cron payloads — worker consuming messages that hit command/SQL/URL sinks (
exec(job.cmd),query(task.sql)). Producers are not trust boundaries. - Exports — data written to CSV/HTML opened in Excel/Sheets: formula injection (
=cmd|...,@SUM() unless-/=/+/@prefixes are stripped. - Upstream service headers/bodies on internal calls — see trusted-header rules in
../auth-review/SKILL.md.
Query-builder raw APIs are SQL sinks too — the ORM does not save you at the raw boundary:
rg -n "\.raw\(|\.literal\(|knex\.raw|sequelize\.literal|typeOrm.*\.query\(|createNativeQuery|cursor\.execute\(\s*f?[\"'].*\{|MyBatis.*\$\{" knex.raw('... ' + req.query.sort), sequelize.literal(...) with interpolation, createNativeQuery concat, MyBatis ${} (vs safe #{}) — all equal to string-built SQL.
Multi-line construction: single-line greps miss queries built across lines (triple-quoted f-strings, chained concatenation). Run bounded multiline scans on the DB layer only — they're slow on whole repos:
rg -nU "execute\(\s*[frb]?['\"]{3}[\s\S]{0,300}(SELECT|INSERT|UPDATE|DELETE)[\s\S]{0,300}(\$\{|%s|\+|f['\"]|#\{)" app/ db/ models/ 2>/dev/nullCategories & quick greps
SQL / NoSQL injection
rg -n -i "(SELECT|INSERT|UPDATE|DELETE).*(\$\{|%s|%d|\+|f['\"]|\.\s*\$|#{|\"\s*\+)" -g '!*.min.js' -g '!test/**'
rg -n "\\\$where|\\\$expr|MongoClient.*eval|mapReduce" # NoSQL: JS-in-queryConfirm the value is bound (?, $1, :param, .execute(sql, params)) vs concatenated/interpolated. String-built queries that only touch constants are noise.
Split construction defeats the single-line grep (drill finding, gaps4): the SELECT literal on one line, the taint on another (const base = 'SELECT ...'; pool.query(base + req.params.ref)). Run the CALL-SITE grep too:
rg -n "(query|execute)\(\s*\w+\s*\+" -g '*.js' -g '*.ts'NoSQL operator injection: in document stores the query object IS the API — passing request data straight through lets attackers inject operators. User.find({ password: req.body.password }) accepts {"password": {"$ne": ""}} → auth bypass; $where/$function = JS execution in the DB.
rg -n "(find|findOne|findOneAndUpdate|updateOne|deleteOne|aggregate)\(\s*\{\s*\.\.\.(req|ctx|event)\.|\{\s*\.\.\.(body|query|params)|find(One)?\(\s*\{[^}]*req\.(body|query)"Report when a request object/field reaches a Mongo-style query without operator stripping (mongo-sanitize, allowlisted keys, $eq-coerced values, or an ODM schema that rejects operators). Safe-looking code that builds { [field]: value } with user-controlled field is the same bug.
OS command injection
rg -n "os\.system|subprocess\.(call|run|Popen)\(.*shell\s*=\s*True|child_process\.(exec|execSync)|spawn\([^)]*shell:\s*true|Runtime\.getRuntime\(\)\.exec|system\(|popen\(|exec\.Command\(.*\+"PHP sinks are case-INSENSITIVE: SyStem(, EXEC(, PoPen( are the
same calls — run the PHP sink census case-insensitively or the
mixed-case spelling evades it (community-rule confirmation: semgrep's
php exec-use went case-insensitive 2026-10 for exactly this):
rg -n -i "\b(system|exec|passthru|shell_exec|proc_open|popen)\s*\(" -g '*.php'
rg -n -i "(system|passthru|shell_exec|proc_open|popen)\s*\(\s*['\"][^'\"]*['\"]\s*\.\s*\$_(GET|POST|REQUEST)" -g '*.php'exec("ls " + filename) = classic. Safe: execFile(cmd, [args]), subprocess.run([cmd, arg]) without shell=True. spawn(cmd, [args], { shell: true }) is NOT the safe shape — the shell re-parses the array (drill finding, gaps4).
XSS
rg -n "innerHTML|insertAdjacentHTML|outerHTML\s*=|createContextualFragment|document\.write|v-html|dangerouslySetInnerHTML|\.html\(|render_template_string|autoescape\s*=\s*False|\|safe\b|markupsafe\.Markup"
rg -n -i "<%=.*request|res\.send\(.*req\.(body|query|params)"Flag reflected user input into any of these sinks. Framework auto-escaping (Jinja2, React JSX text nodes) is a valid defense — but dangerouslySetInnerHTML/v-html/|safe bypass it.
Fires-but-safe forms need a falsifier, not a pass. innerHTML hits that are DOMPurify-sanitized (el.innerHTML = DOMPurify.sanitize(x)), clearing (= ''), or code-owned constants are SAFE — each report of a real finding must state which of these it ruled out and how. Comment-text mentions of sink names are documentation noise: disposition them, don't report them.
Census + privilege direction — the rules every framework XSS check below points to. Disposition every sink hit (finding / verified-safe / not-assessed); severity is set by WHO writes the value vs WHO renders it, not by the sink alone:
| Author of the raw content | Viewer | Severity |
| Unprivileged (student/customer/guest: reviews, tickets, messages, quiz answers, display names, uploaded filenames) | Privileged (admin/moderator/staff view, support desk, HTML email render) | Critical — fires in the staff origin → session riding → account takeover; approval/moderation queues GUARANTEE a privileged viewer opens it |
| Privileged-authored (admin/instructor: descriptions, embeds, templates) | Unprivileged/everyone | High (public) / Medium (authenticated) |
| Same privilege both sides | same | Medium |
Moderation/approval workflows are delivery mechanisms, not mitigations (see ../course-platform-security/SKILL.md §6.5 and ../laravel-security/SKILL.md Step 4).
Sanitizer/encoder ≠ safe by default. When an escape/purify layer sits between source and sink, do not close the cell as verified-safe on its presence alone — verify bypass resistance (GHSL-2026-072: a CMS's HTML encoding function itself had an encoding-bypass bug → XSS). Presence of DOMPurify/e()/encoder helpers earns 🟡 needs-verification, not 🟢; check the encoder is applied on the read path, with the right context (HTML vs attribute vs URL), and against known bypass shapes for that library's version.
Path traversal
rg -n "open\(|readFile|writeFile|sendFile|send_file|File\(|Path\.Combine|require\(.*\+|include\(.*\$|fopen\("Flag when user input influences the path and there's no path.resolve + allowlist prefix check / send_file(..., safe=True) equivalent.
SSRF
rg -n "requests\.get\(|axios\.(get|post)|fetch\(|urllib\.request|HttpClient|curl_exec|http\.Get\("Flag only when the URL (host or full) derives from user input — e.g. "fetch this webhook URL", URL preview features, image import by URL. Check for allowlist/redirect limits/private-IP blocking.
Deferred/stored SSRF (registered callbacks) — the sneakiest form: the URL is captured at REGISTRATION time (webhook/callback/integration settings, OAuth redirect_uri stored per-app) and fetched later by a delivery/retry job. At fetch time no request validator applies — the stored value IS the attack.
rg -n "(webhook|callback|notify|hook)[_a-z]*(url|uri|endpoint)" -g '*.js' -g '*.ts' -g '*.py' -g '*.java' -g '*.rb' -g '*.php'
rg -n "(fetch|axios|requests\.get|http\.Get|RestTemplate|curl)\(\s*\w*[Hh]ook\w*\.?(url|uri|endpoint)" | headChecks: registration validates scheme+host (public DNS, no private ranges, no wildcard subdomain tricks) AND blocks internal/metadata ranges at FETCH time (DNS rebinding defeats registration-time-only checks — revalidate after resolve). Event payloads delivered to attacker-registered URLs must not contain secrets/tokens (exfil channel). Missing any → High (Critical on cloud where metadata is reachable).
Egress controls (defense verification, A10): when the app fetches user-influenced URLs, also check the environment makes SSRF expensive:
- Kubernetes: any
NetworkPolicyrestricting egress for the fetcher pods? (absence → note; pair with the SSRF finding — see../container-iac-security/SKILL.md) - Cloud: metadata service reachable with v1 tokens (AWS
metadata_options http_tokens = "optional", no hop limit) → cloud-metadata SSRF = credential theft, raise SSRF severity - No egress proxy/allowlist at all → mention as hardening: the fix lives in network config, not app code
rg -n "http_tokens|metadata_options|hop_limit" -g '*.tf' -g '*.yaml' -g '*.json'
rg --files -g '*networkpolicy*' -g '*NetworkPolicy*' ; rg -n "kind: NetworkPolicy|policyTypes:.*Egress" -g '*.yaml' -g '*.yml'Deserialization
rg -n "pickle\.loads?|yaml\.load\((?!.*Loader=)|marshal\.loads|ObjectInputStream|readObject|unserialize\(|eval\(|exec\(|new Function\(|Function\("yaml.load without SafeLoader, pickle.loads on user-controlled bytes, PHP unserialize on user input, eval on anything remote = CRITICAL.
CRLF / header injection
rg -n "setHeader\(|addHeader\(|res\.set\(|header\(\s*['\"]Location|redirect\(.*\+\s*req|Location.*\+ *request"User-controlled data (query params, URL paths, filenames, webhook fields) written into response headers → \r\n in it splits/adds headers: ?next=/%0d%0aSet-Cookie: admin=1, response splitting on proxies, poisoned caches, injected email headers when the same data feeds mail APIs. Report MEDIUM (HIGH when the header is Location/Set-Cookie or feeds an email/sms gateway). Fix: strip \r\n (and \0) from values before any header/mail use, URL-encode redirect components.
Identifier & allowlist comparison hygiene (unicode)
String equality is not identity: NFC/NFD forms collide visually but compare unequal (or the reverse — stored NFD, compared NFC), and IDN homoglyphs defeat eyeball-validated allowlists.
rg -n "(normalize|NFC|NFD|NFKC)" -g '*.js' -g '*.py' | head # absent = check comparisons manually
rg -n "(=== req\.|== req\.|\.includes\(.*(host|origin|domain|name))" -g '*.js' -g '*.ts' | head- Username/account lookups by exact match without unicode normalization → account spoofing and admin-lookalike registrations (
admın, soft-hyphen suffixes) → Medium/High (the Django CVE-2019-19844 class — see vuln-db) - Host/origin allowlists compared as raw strings: verify they anchor exactly (
.endsWith('example.com')matchesevilexample.com) AND reject mixed-script/punycode hosts unless explicitly allowed (URL.hostkeeps punycode — compare against the punycode form, not the human form) - Path allowlists:
path.normalizedoes NOT unicode-normalize — fullwidth slashes/dots (\uFF0F,\uFF0E) can slip past naive prefix checks before the OS layer interprets them → flag prefix checks that don't canonicalize aggressively
Fixes: normalize identifiers to NFKC at write AND query time; compare origins via parsed URL with exact-host equality; canonicalize paths with resolved-containment checks (above).
Template injection (SSTI)
rg -n "render_template_string|Template\(.*\+|Jinja2\(.*from_string|erb\.new\(.*\+|Mustache\.render\(.*\+|StringTemplate|do_shortcode\s*\("User input inside the template string (not the data context) = SSTI. WordPress shortcodes are the PHP flavor: do_shortcode($_POST[...]) executes any shortcode the site's plugins register — unauthenticated arbitrary shortcode execution (class incident: LatePoint CVE-2026-92966; entry: ../cve-research/vuln-db/entries/2026-10-01-cve-2026-92966.md).
Prototype pollution (Node/JS)
rg -n "(__proto__|constructor\s*[\[.:]|\.prototype\s*\[)"
rg -n "(deepMerge|defaultsDeep|merge\(|extend\(|\.set\()\(.*req\.(body|query|params)|Object\.assign\(\s*\{\}\s*,\s*req\.(body|query)"
rg -n "urlencoded\(\s*\{\s*extended:\s*true|bodyParser\(\s*\)|query\s*parser" # deep-parsed bodies are the usual sourceAny recursive merge / _.set / Object.assign that copies user-controlled keys into existing objects lets __proto__/constructor.prototype overwrite built-ins → pollutes every object, chains to RCE via gadgets (child_process + NODE_OPTIONS, EJS, template engines). Report as High (Critical when merged config feeds templating/process spawning). Fixes: allowlist-pick fields before merging, strip __proto__/constructor/prototype keys, Object.create(null) targets, app.set('query parser', 'simple').
XXE / unsafe XML parsing
rg -n "DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|XMLReader" # Java — check hardening
rg -n "etree\.(parse|fromstring)|xml\.etree|minidom|pulldom|sax\.|expat|simplexml|DOMDocument|libxml|libxmljs|Nokogiri|REXML"- Java factory parsing request XML without
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)→ Critical (file read, SSRF via parameter entities, billion-laughs DoS). - Python stdlib
xml.*on user XML → internal entity expansion (billion laughs) → High; fix isdefusedxml. lxml.etreewith default parser resolves entities (incl. externalfile://) → Critical; fixXMLParser(resolve_entities=False)or defusedxml.- PHP
libxml< 2.9 /libxml_disable_entity_loader(false)era stacks → flag. Goencoding/xmlis entity-safe; don't flag.
ReDoS
rg -n "\(\s*[^()]*[+*]\s*\)[+*{]|(\[[^\]]*[+*][^\]]*\])[+*{]" # heuristic: nested/starred quantifiers
rg -n "\.test\(|\.match\(|\.replace\(|\.search\(|re\.(match|search|fullmatch|sub)|Pattern\.matches|regexp\.MustCompile"Cross the two lists manually: report only when a nested-quantifier regex ((a+)+b, (a|a)*$, ([a-z]+)*d) runs on user-controlled, unbounded-length input → event-loop/thread starvation → High (Medium for capped-length inputs). Safe: bounded input length, linear engines (Go RE2, re2 bindings), single non-nested quantifier. Verify the pattern really nests quantifiers before reporting — this heuristic over-fires.
Open redirect
rg -n -i "res\.redirect\(|redirect_to |redirect\(request|Redirect\(.*param|sendRedirect|window\.location(\.href)?\s*=|location\.replace\(|header\(\s*['\"]Location|RedirectToAction"
rg -n -i "\?(next|continue|returnTo|returnUrl|redirect|redirect_uri|url|goto)=" # in views/handlersFlag when the redirect target comes from user input with no exact-host allowlist. Login/logout/reset flows carrying ?next= params are the classic; High when the URL carries tokens/codes (OAuth code, reset token) or silences origin checks, otherwise Medium. Safe: exact allowlist of destinations, or relative-only after new URL(next, base) origin check — prefix/suffix matching on the host is bypassable (evil.com vs evil-example.com).
Unsafe file upload
C/C++ native components: memory-safety and native injection sinks (strcpy/sprintf, format strings, system(), integer-overflow allocs, TOCTOU) live in references/patterns.md "C/C++ native code" — load it when the repo carries .c/.cc/.cpp service/extension code.
rg -n "multer|multipart|form\.File|FormFile|MultipartFormDataEntry|\.originalname|file\.filename|Storage\(|upload"For every upload handler check:
- Type allowlist — trusting
Content-Typeor the client filename alone is bypassable (double extensionavatar.php.png;x.svg= stored XSS;.shtml). Extension-derivation bypass (2 real-world WP CVEs same week, 2026-10: Contact Manager CVE-2026-109218 + SimpleForm CVE-2026-118235): a trailing dot (shell.php.) makes derivation return EMPTY so any blacklist misses, thensanitize_file_name()-style cleanup strips the dot and the stored name reacquires the dangerous suffix — derive from the FINAL stored filename, REJECT empty/ambiguous derivations, and allowlist (blacklist-only is the repeated failure mode; also check every validation RESULT is actually enforced beforemove_uploaded_file— Pie Register CVE-2026-84999 checked then ignored) - Storage location — files written under webroot/static/public → served/parsed by the web server = Critical (parse-to-RCE chains); non-served storage → High
- Filename — user filename used verbatim (
../../traversal, collisions) instead of CSPRNG-generated names - Caps — no size limit / no per-entry decompression cap → zip bomb (
zipfile.extractallunchecked) → High - Extraction attacks (distinct from bombs): entry names containing
../(zip-slip:adm-zip/extractAllTo,zipfile.extractall,tarfile.extractallwrite outside the target dir) and symlink members (a tar symlinklink -> /etc/passwdfollowed by a later member that overwrites it —tarfilepre-3.12 style). Safe extraction: validate EVERY entry's resolved path stays inside the target (path.resolve(dest, name).startsWith(dest + sep)), reject absolute/symlink members, prefer libraries that refuse by default (zipfilewith custom member filter,libarchivehardened flags) → High/Critical - Serving — uploads served
inlinewithoutX-Content-Type-Options: nosniff/Content-Disposition: attachment; SVG uploads rendered (embed<script>, XXE via<externalEntity>). MIME-spoofing stored XSS (GHSL-2026-052, Docmost): a client-declaredContent-Typeis attacker data — an upload that passes extension allowlists but is served inline sniffed/rendered astext/htmlexecutes. Verify: served type is forced from a server-side mapping (never from the request), magic-byte check for ambiguous types, andnosniffon every user-content response.
Reporting
Each confirmed finding: severity (SQLi/command/deserialization/SSTI/XXE reachable unauthenticated = Critical; prototype pollution = High→Critical with gadget; open redirect = Medium, High with tokens in URL; ReDoS = Medium/High), file:line, the source→sink trace in two lines, and the fix: parameterize / argument-array exec / auto-escape / allowlist + canonicalize / defusedxml / bounded regex / exact-host redirect allowlist.
False-positive discipline: hits inside test/, examples/, migrations/, or on constant strings get dropped — note the count of dropped hits in the summary so effort is visible.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

