Injection beyond SQL: NoSQL, LDAP, XPath, CRLF and SSI
Every query language is injectable, and each one has a different syntax for always-true. Mongo operator injection, LDAP filter injection, XPath blind extraction, header injection through CRLF, and the server-side includes nobody remembers exist.
SQL injection gets the attention, but the underlying mistake - building a query by concatenating untrusted input - is language-agnostic. Anywhere the application constructs a query, a filter, a path expression, or a header from your input, the same class exists with different syntax.
The method transfers exactly. Find the injection point, break the syntax to confirm it, construct an always-true expression to bypass, then build an extraction channel - visible, blind, or timing. Only the vocabulary changes, and this post is the vocabulary.
NoSQL: injecting objects, not strings
MongoDB queries are documents, not text, so the classic quote-breaking does not apply. The bug instead is that a JSON body or a PHP query string can supply an object where the developer expected a string - and MongoDB's operators are just keys in that object.
{"user": "admin", "pass": "guess"}
{"user": "admin", "pass": {"$ne": null}} // password is not null: true
{"user": {"$regex": "^a"}, "pass": {"$ne": 1}} // enumerate usernames by prefix
{"user": "admin", "pass": {"$gt": ""}} // any string sorts above ""req.body.pass is passed to the driver without a type check.- In a JSON body: substitute an object for any string value.
$ne,$gt,$regex,$in,$existsare the useful ones. - In a form body or query string: PHP and some Node parsers build arrays from bracket syntax, so
pass[$ne]=1produces the same object. This is the version that catches people out, because the request looks like plain form data. - Blind extraction with
$regex: you cannot see the password, but you can see whether login succeeded.{"$regex": "^a"}narrows one character at a time - the same bisection as blind SQL injection. $whereand$function: if the application uses them, the query contains server-side JavaScript and injection there is direct code execution.{"$where": "sleep(5000) || true"}both proves it and gives you a timing channel.- Operator injection in aggregation:
$lookupreaches other collections, which is how a query scoped to one tenant reads another's.
LDAP: the filter is a parenthesised expression
LDAP authentication builds a filter like (&(uid=USER)(password=PASS)). The syntax uses parentheses, &, |, ! and *, and none of them are escaped by string concatenation.
uid = * -> (&(uid=*)(password=PASS)) any user
uid = *)(uid=* -> (&(uid=*)(uid=*))(password=PASS)) password clause discarded
uid = admin)(& -> (&(uid=admin)(&))(password=PASS)) the (&) is always trueBlind extraction uses * as a wildcard: uid=admin)(description=A* succeeds only if the description starts with A. Walk the alphabet, one character at a time, and you read attributes you were never authorised to see. LDAP shows up in CTFs as an authentication backend and in enterprise-flavoured challenges as a directory search.
XPath: querying the XML document itself
When user records live in an XML file, authentication is often //user[name='USER' and pass='PASS']. Quote-breaking works exactly as in SQL:
' or '1'='1 classic bypass
' or 1=1 or ''=' for numeric contexts
'] | //* | //foo[' select every node in the documentThat last payload is the reason XPath injection is often worse than SQL injection: XPath has no access control and no notion of tables you are allowed to query. A union-style payload reads the entire document, credentials included. Blind extraction uses string-length() and substring():
' or string-length(//user[1]/pass)=8 or ''='
' or substring(//user[1]/pass,1,1)='a' or ''='If the XML is parsed rather than queried, you may have XXE instead - and it is worth testing for both, since they live in the same code.
CRLF injection: writing your own headers
Any value reflected into a response header - a redirect Location, a Set-Cookie, a custom X- header - is a header injection point if \r\n survives. Two newlines end the header block entirely and everything after is a response body you control.
/redirect?to=/home%0d%0aSet-Cookie:%20session=attacker
/redirect?to=%0d%0a%0d%0a<script>alert(1)</script>- Response splitting is the severe form: inject a complete second response, and if a cache is in front, it may be stored and served to other users. Same outcome as request smuggling, reached differently.
- Log injection is the same character in a different sink. A
\nin a logged field forges log lines, which matters when the challenge asks you to hide from a log-analysis step. - SMTP header injection is CRLF in an email field. A
\nBcc:in a contact form adds recipients; a\n\nstarts the message body. - Encoding matters: try
%0d%0a,%0a,%E5%98%8A%E5%98%8D(Unicode characters some parsers narrow into CR and LF), and a raw newline where the transport allows it.
Server-side includes and their relatives
SSI is a directive syntax that some web servers still process in .shtml files, and it executes commands:
<!--#exec cmd="id" -->
<!--#include virtual="/etc/passwd" -->
<!--#echo var="DOCUMENT_ROOT" -->It is rare in production and common in CTFs, precisely because it is forgotten. Test it wherever your input ends up in a page served by Apache or nginx with SSI enabled, and note the related ESI (Edge Side Includes), processed by some CDNs and proxies, which offers <esi:include src="..."/> - an SSRF primitive that fires at the edge rather than at the origin.
The others, briefly
| Language | Injection point | Always-true / probe |
|---|---|---|
| GraphQL | A filter or ordering argument passed to an ORM | Introspection first - see hacking APIs. |
| Elasticsearch / Lucene | A search box passed to query_string | *:*, or _source field selection to read excluded fields. |
| Regex (ReDoS) | A user-supplied pattern | (a+)+$ against a long non-matching string - a denial of service, and a timing oracle. |
| Format strings in Python | "...".format(user) or an f-string on user input | {0.__class__.__init__.__globals__} - a sandbox escape, not a memory bug. |
| Log4j-style lookups | Any logged string | ${jndi:ldap://...} and ${env:SECRET} - the second works even where JNDI is patched. |
| Mail / sendmail arguments | A From address concatenated into a command | -X/var/www/html/x.php - argument injection. |
The method that transfers between languages
Work out what the input becomes
A filter, a document, a path expression, a header, a template. That single question selects the syntax.
Break it
Send the metacharacter for that language and look for an error, a different length, or a changed behaviour. A quote for SQL and XPath, a parenthesis for LDAP, an object for Mongo, a CR for a header.
Neutralise the rest of the expression
Every language has a comment, a wildcard, or a trailing clause that swallows what follows.
Construct always-true
'1'='1',(&),{"$ne": null},*. Same idea, five spellings.Build an extraction channel
Visible output if you are lucky; otherwise a boolean condition and bisection, or a timing primitive.
Check the type before assuming the type
Half of NoSQL injection is realising the parameter can be an object, and half of CRLF injection is realising the value reaches a header.
The reason to learn the whole family rather than just SQL is recognition speed. When a login form rejects ' OR 1=1 and you notice the backend is Node with Mongo, the right next request is {"$ne": null} and not another forty SQL payloads. Knowing which language you are injecting into is most of the work.