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. - `$where` and `$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.