Give each query language its own boundary. In MongoDB, attackers send an object such as {"$ne": null} where you expected a string, so check the type and compare with $eq. Escape LDAP filter values with your library's RFC 4515 function. Pass XPath values as variables instead of quoting them into the expression. Never evaluate SpEL, OGNL or other expression-language strings that contain request data; store user rules as data and evaluate them in code.
- Do check that every MongoDB filter value is the scalar type you expect; parsed JSON can carry
{"$ne": null}. - Do compare with
{$eq: value}or enable MongoosesanitizeFilteras a second layer, and disable server-side JavaScript you don't use. - Do escape LDAP filter values with
filter_format,escapeFilteror JNDI{0}arguments, and reject empty passwords before a bind. - Do pass XPath values as variables such as
$sku; never splice quotes into the expression. - Don't evaluate user-controlled SpEL, OGNL or EL; Spring says
StandardEvaluationContextmust never see untrusted expressions. - Don't rely on middleware that strips
$keys; validate types and allowlist field names in the handler.
| Setting | Value | Note | Source |
|---|---|---|---|
MongoDB security.javascriptEnabled | false (default is true) | Same as --noscripting. Disables $where, $function, $accumulator and mapReduce; verified on MongoDB 8.2.6 and 9.0.2. | https://www.mongodb.com/docs/manual/core/server-side-javascript/ |
Mongoose sanitizeFilter | true (default is false) | Wraps nested objects with $-prefixed keys in $eq and refuses $where; mongoose.trusted() exempts a known selector. | https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.set() |
| Express 5 query parser | "simple" (the Express 5 default) | "extended" (qs) turns code[$ne]=x into a nested object. Repeated keys become arrays in both modes. | https://expressjs.com/en/guide/migrating-5.html |
express.urlencoded() extended | false (the Express 5 default) | true turns code[$ne]=x into {code: {$ne: "x"}}. | https://expressjs.com/en/guide/migrating-5.html |
| LDAP simple bind with an empty password | Reject in the application before binding | RFC 4513 section 5.1.2 unauthenticated bind. OpenLDAP 2.6.8 refused it with unwillingToPerform, but the RFC only says servers SHOULD. | https://www.rfc-editor.org/rfc/rfc4513.html#section-5.1.2 |
| SpEL evaluation context for non-constant expressions | SimpleEvaluationContext.forReadOnlyDataBinding() | Spring describes its limits as best effort; StandardEvaluationContext must never evaluate an untrusted expression. | https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html |
| SpEL maximum expression length | 10,000 characters (default) | Set through SpelParserConfiguration or spring.context.expression.maxLength. | https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html |
Struts struts.ognl.expressionMaxLength | 256 (documented default); keep it small | Also enable struts.allowlist.enable, and never use request values in forced %{...} evaluation. | https://struts.apache.org/security/ |
- POST
{"code": {"$ne": null}},{"code": {"$regex": "^A"}}and{"code": ["x"]}to every JSON endpoint that queries MongoDB; expect 400, never a match. - Run a
$wherequery against each environment; with server-side JavaScript disabled it fails withno globalScriptEngine in $where parsing. - Send
*,a*andalice)(uid=*to LDAP-backed lookups and logins; expect no match, and confirm an empty password returns false without a bind. - Send
x' or '1'='1to XPath-backed lookups and confirm every XPath expression is a constant in code. - Search for
parseExpression,StandardEvaluationContextand OGNL evaluation of request data, and run CodeQL or Semgrep rules for NoSQL, LDAP, XPath and expression injection.
- Applies to
- MongoDB Server 8.2.6 and 9.0.2 tested
- MongoDB Node.js driver 7.7.0 tested
- Express 5.x (5.3.0 tested)
- Mongoose 9.11.1 tested
- PyMongo / Pydantic 4.18.3 / 2.14.0 tested
- python-ldap with OpenLDAP 3.4.8 with slapd 2.6.8 tested
- ldapts escapeFilter in 8.2.0 and later (9.2.0 tested)
- lxml / npm xpath 6.1.3 / 0.0.35 tested
- Spring Framework spring-expression 7.0.9 on Java 21 tested
Code that builds MongoDB queries, LDAP filters, XPath expressions or expression-language strings from input. Escaping rules differ for each query language.
- Part of
- Workflow: Fix a finding
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Explain why MongoDB operator injection depends on value types rather than quotes.
- Use the right escaping or binding API for LDAP filters and XPath.
- Replace evaluation of user-supplied expressions with rules stored as data.
- Write regression tests that run against the real query engine.
Before you start
Backend developers and AppSec reviewers who work with MongoDB, directory services, XML data or Java expression languages.
- Read request handlers in JavaScript, Python or Java.
- Know how parameterized SQL queries separate data from syntax.
Injection happens when input crosses into a language that another component interprets. SQL gets most of the attention, but the same failure shows up in MongoDB query documents, LDAP search filters, XPath expressions and expression languages such as SpEL and OGNL. Each has its own special syntax, so each needs its own fix. A generic sanitizer that strips a few characters cannot cover them all.
The table maps each grammar to the input an attacker sends, the primary fix and a backstop. Fixing the grammar does not settle authorization. A query that can no longer be rewritten can still return a record the caller should not see, so keep tenant and owner conditions in the query.
| Grammar | Attacker input | Primary fix | Backstop |
|---|---|---|---|
| MongoDB query document | {"$ne": null}, {"$regex": "^a"}, a $where string | Require scalar types and build the filter from named fields | $eq, Mongoose sanitizeFilter, javascriptEnabled: false |
| LDAP search filter | *, )(, alice)(attr=x* | RFC 4515 escaping through the library | Least-privilege bind account, size limit |
| LDAP distinguished name | , + = \ " < > ; | RFC 4514 DN escaping, or the DN the server returned | Search-then-bind |
| XPath 1.0 | x' or '1'='1 | XPath variables such as $sku | Expressions kept as constants |
| SpEL, OGNL, EL | T(java.lang.Runtime)..., %{...} | Do not evaluate request data; store rules as data | SimpleEvaluationContext, length limits, framework allowlists |
MongoDB filters are documents, not strings, so quotes are not the issue. The issue is that parsed JSON can put an object where the code expects a string. If a handler runs findOne({ code: req.body.code }) and the body is {"code": {"$ne": null}}, the driver sends a query operator, and the filter now means "any code that is not null". {"$gt": ""} does the same. {"$regex": "^K"} returns a yes or no answer, which lets an attacker recover a secret one character at a time.
Which inputs can carry objects depends on the parser. express.json() always can. In Express 5 the default query parser ("simple") and express.urlencoded() with its new default of extended: false do not nest keys, so code[$ne]=x arrives as a key literally named code[$ne]. Switching either one to the qs-based extended mode turns it back into {code: {$ne: "x"}}. Repeated keys such as ?code=a&code=b produce an array in every mode. Flask's request.get_json() and other APIs that return the raw parsed body have the same exposure.
The route below redeems a single-use invite code. Against MongoDB 8.2.6 and 9.0.2, {"$ne": null} redeemed an invite without knowing any code, and {"$regex": "^Z"} returned 200 for the first letter of a real code and 404 for other letters.
// invites-vulnerable.mjs: Express 5 and the MongoDB Node.js driver
import express from 'express';
export function mountRedeem(app, invites) {
app.post('/invites/redeem', express.json(), async (req, res) => {
// req.body.code can be any JSON value, including {"$ne": null}
const invite = await invites.findOneAndUpdate(
{ code: req.body.code, used: false },
{ $set: { used: true } },
);
if (!invite) return res.status(404).json({ error: 'unknown code' });
res.json({ team: invite.team });
});
}Check the type and shape the feature needs before the value reaches the query. Here that means a string that matches the invite format. For larger bodies, a schema validator such as Ajv, Zod or Pydantic does the same job. A Pydantic 2.14 model with code: str rejected an operator object, an integer and a list.
Then make the comparison literal. { code: { $eq: code } } matches only documents whose code equals the value. If a later edit removes the type check, an object such as {"$ne": null} is compared as a literal document and matches nothing; both server versions returned no documents for it. Mongoose does this wrapping for you after mongoose.set('sanitizeFilter', true). In Mongoose 9.11 that setting turned the same payloads into cast errors and refused $where filters.
Build filters from named fields. Do not pass req.body or req.query to find(). If users choose the field or the operator, map an allowlisted name such as status to a fixed field, and an allowlisted word such as before to $lt. A user-supplied key can be $where or $expr, which turns data into an operator at the top level of the filter.
// invites-fixed.mjs
import express from 'express';
const CODE = /^[A-Z0-9]{4}-[A-Z]{3,8}$/;
export function mountRedeem(app, invites) {
app.post('/invites/redeem', express.json({ limit: '1kb' }), async (req, res) => {
const code = req.body?.code;
// 1. Accept only the type and shape this feature needs.
if (typeof code !== 'string' || !CODE.test(code)) {
return res.status(400).json({ error: 'invalid code' });
}
// 2. $eq keeps the value literal even if a later edit drops step 1.
const invite = await invites.findOneAndUpdate(
{ code: { $eq: code }, used: false },
{ $set: { used: true } },
);
if (!invite) return res.status(404).json({ error: 'unknown code' });
res.json({ team: invite.team });
});
}$where, $function, $accumulator and mapReduce run JavaScript inside mongod. A $where string built from input, such as "this.owner == '" + owner + "'", adds classic string injection to the operator problem. The payload sleep(300) || true held a query for at least 300 ms, so a few requests can tie up the server.
MongoDB recommends $expr with ordinary aggregation operators instead of $where, because $expr does not run JavaScript. If the application does not use these operators, turn server-side scripting off. With javascriptEnabled: false, MongoDB 9.0.2 rejected both $where and $function. Treat client-supplied aggregation pipelines the same way: $lookup can read other collections, and $out and $merge write to them.
# mongod.conf
security:
javascriptEnabled: false # same effect as the --noscripting flagAn LDAP search filter is a string such as (&(objectClass=inetOrgPerson)(uid=alice)). Splicing a username into that string hands the filter syntax to the user. Against OpenLDAP 2.6.8, the input * returned every person. The input alice)(departmentNumber=e* added a second condition, so the result revealed whether Alice's department starts with e. Repeating the question reveals the whole value.
RFC 4515 requires five characters in a filter value to be escaped as a backslash and two hex digits: * as \2a, ( as \28, ) as \29, \ as \5c and NUL as \00. Use the library's function rather than your own replace calls: filter_format() or escape_filter_chars() in python-ldap, the escapeFilter tagged template in ldapts 8.2 and later (filter objects such as EqualityFilter in older releases), and {0} placeholders with a filterArgs array in Java's DirContext.search(), which escapes each argument. Distinguished names follow different rules (RFC 4514), so a filter escaper does not protect a DN. The simplest way to avoid building DNs is to search for the entry and use the DN the server returns.
# directory.py: python-ldap 3.4 search-then-bind
import ldap
import ldap.filter
PEOPLE = "ou=people,dc=example,dc=com"
def find_person(conn, uid):
"""Return at most one entry; uid can never change the filter."""
if not isinstance(uid, str) or not 1 <= len(uid) <= 64:
raise ValueError("invalid uid")
flt = ldap.filter.filter_format(
"(&(objectClass=inetOrgPerson)(uid=%s))", [uid])
return conn.search_ext_s(PEOPLE, ldap.SCOPE_ONELEVEL, flt,
["cn", "mail"], sizelimit=2)
def authenticate(uri, service_conn, uid, password):
if not isinstance(password, str) or password == "":
return False # an empty password would be an unauthenticated bind
entries = find_person(service_conn, uid)
if len(entries) != 1:
return False
user_conn = ldap.initialize(uri)
try:
user_conn.simple_bind_s(entries[0][0], password) # DN from the server
return True
except ldap.INVALID_CREDENTIALS:
return False
finally:
user_conn.unbind_s()Do not check passwords inside a filter such as (userPassword=...). Search for the entry with an escaped filter, require exactly one match, and then bind as that entry's DN with the supplied password. A size limit of two stops a filter that unexpectedly matches many entries.
Reject an empty password before calling bind. RFC 4513 section 5.1.2 calls a simple bind with a name and an empty password an unauthenticated bind: it grants anonymous access and proves nothing about the user. The RFC says servers should refuse it by default, and OpenLDAP 2.6.8 did, with unwillingToPerform. That is a recommendation, not a guarantee, so the application has to check. Give the lookup account read access only to the attributes the feature needs.
The Node.js version below uses ldapts and passed equivalent tests against the same OpenLDAP server.
// directory.mjs: ldapts 8.2+ search-then-bind
import { Client, InvalidCredentialsError, escapeFilter } from 'ldapts';
export async function authenticate(url, service, uid, password) {
if (typeof uid !== 'string' || typeof password !== 'string' || password === '') {
return false;
}
const svc = new Client({ url });
let dn;
try {
await svc.bind(service.dn, service.password);
const { searchEntries } = await svc.search('ou=people,dc=example,dc=com', {
scope: 'one',
filter: escapeFilter`(&(objectClass=inetOrgPerson)(uid=${uid}))`,
attributes: ['cn'],
sizeLimit: 2,
});
if (searchEntries.length !== 1) return false;
dn = searchEntries[0].dn;
} finally {
await svc.unbind();
}
const user = new Client({ url });
try {
await user.bind(dn, password);
return true;
} catch (err) {
if (err instanceof InvalidCredentialsError) return false;
throw err;
} finally {
await user.unbind();
}
}XPath 1.0 has no escape sequence for a quote inside a string literal. The only workaround is to split the value with concat(), which is easy to get wrong, so do not build an expression such as //product[@sku='...'] from input. The input x' or '1'='1 turns the predicate into (@visibility='public' and @sku='x') or '1'='1', which is true for every product, including internal ones. With substring(), the same trick becomes a yes or no oracle for hidden values.
Most XPath libraries accept variables. Write $sku in the expression and pass the value separately: lxml takes keyword arguments on xpath() or on a compiled etree.XPath, the npm xpath package takes a variables object in parse(expression).select({ node, variables }), and Java's javax.xml.xpath accepts an XPathVariableResolver. The value is then a string, never syntax. The test below confirms that a quote is ordinary data and that the internal product stays hidden.
# catalog.py: lxml 6, XPath 1.0
from lxml import etree
CATALOG = etree.fromstring(b"""<catalog>
<product sku="A-100" visibility="public"><name>Desk lamp</name></product>
<product sku="B-200" visibility="internal"><name>Prototype charger</name></product>
</catalog>""")
def find_vulnerable(sku): # do not use: input becomes XPath source
return CATALOG.xpath(f"//product[@visibility='public' and @sku='{sku}']")
FIND = etree.XPath("//product[@visibility='public' and @sku=$sku]")
def find(sku):
return FIND(CATALOG, sku=sku) # sku is bound as a string variable
probe = "x' or '1'='1"
assert [p.get("sku") for p in find_vulnerable(probe)] == ["A-100", "B-200"]
assert find(probe) == []
assert [p.findtext("name") for p in find("A-100")] == ["Desk lamp"]
assert find("B-200") == [] # internal items stay hidden
assert find("O'Brien") == [] # a quote is ordinary data
print("PASS")SpEL, OGNL and Jakarta EL can read properties, call methods and create objects. When request data becomes part of an expression, the attacker gets the same powers and can reach the runtime. Spring Cloud Function evaluated a SpEL routing expression supplied by the caller (CVE-2022-22963), and Confluence Server and Data Center had OGNL injection flaws that let unauthenticated attackers run code (CVE-2021-26084 and CVE-2022-26134). All three are in CISA's Known Exploited Vulnerabilities catalog, and NVD maps each to CWE-917.
The fix is to stop evaluating input. If users need custom rules such as "total over 100 and country is NL", store each rule as data, with a field and an operator from allowlists and a typed value, and evaluate it in ordinary code. If you inherit a feature that must evaluate SpEL, use SimpleEvaluationContext.forReadOnlyDataBinding(). It rejects type references and constructors, and it rejects method calls unless you enable them. Spring calls these limits best effort and says StandardEvaluationContext must never evaluate an expression from an untrusted source. SpEL also refuses expressions longer than 10,000 characters by default. Template engines, eval and the limits of in-process sandboxes are covered in /domains/appsec/secure-coding/code-template-injection/.
For Struts, Apache's guidance is to never pass request values to forced %{...} evaluation, to keep struts.ognl.expressionMaxLength small (its documented default is 256), and to enable the OGNL allowlist with struts.allowlist.enable. The Confluence flaws above were product bugs rather than customer code, so patching is part of the control.
// SpelRules.java: spring-expression 7.0
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class SpelRules {
public record Order(String country, int total) {}
static final SpelExpressionParser PARSER = new SpelExpressionParser();
// Vulnerable: full SpEL, including T(...) type references and constructors
static Object evaluateUnsafe(String rule, Order order) {
return PARSER.parseExpression(rule).getValue(new StandardEvaluationContext(order));
}
// Restricted: read-only properties; no types, constructors or method calls
static Boolean evaluateRestricted(String rule, Order order) {
EvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().build();
return PARSER.parseExpression(rule).getValue(ctx, order, Boolean.class);
}
public static void main(String[] args) {
Order order = new Order("NL", 120);
String probe = "T(java.lang.System).getProperty('user.home') != null";
System.out.println(evaluateUnsafe(probe, order)); // true: a static method ran
System.out.println(evaluateRestricted("total > 100 and country == 'NL'", order)); // true
try {
evaluateRestricted(probe, order);
} catch (SpelEvaluationException e) {
System.out.println(e.getMessageCode()); // TYPE_NOT_FOUND
}
}
}Most regressions come from a control that looks complete but sits in the wrong place.
- Key-stripping middleware. Removing keys that start with
$is a denylist, and it can disappear in an upgrade.express-mongo-sanitize2.2.0 throws on every request under Express 5 becausereq.queryis now a getter. A team that removes it to get the app running loses its only check unless the handlers validate types. - Coercing instead of rejecting.
String(value)turns an object into"[object Object]"and hides the attempt. Return 400 so the probe shows up in logs. - Using the wrong escaper. A filter escaper does not protect a DN, HTML encoding does not protect XPath, and SQL escaping protects none of these.
- Passing search text to
$regex. Escape regular-expression metacharacters (ASVS 5.0 requirement 1.2.9) so the text matches literally, or offer prefix search through a fixed operator. - Second-order values. A value stored safely can later be used to build a filter or an expression. Apply the same controls where the stored value is used.
- Verbose errors. Returning a driver or parser error helps an attacker tune a payload. Return a generic 400 and log the detail.
Test against the real interpreter. mongodb-memory-server starts a real mongod inside a unit test, an OpenLDAP container starts in seconds, and lxml and Spring's parser run in process. Assert both sides: the legitimate value still works, and each payload is rejected or matches nothing. The table lists results from the tested snippets in this guide.
In code review, search for the sinks directly: find, findOne, aggregate and update calls that receive req.body or req.query; $where and $function; LDAP search calls whose filter is built with +, f-strings or format(); xpath() or evaluate() calls on built strings; and parseExpression or OGNL evaluation that touches request data.
Static analysis finds string-built queries quickly. CodeQL covers these sinks with js/sql-injection (which includes MongoDB queries), py/nosql-injection, py/ldap-injection, java/ldap-injection, js/xpath-injection, py/xpath-injection, java/spel-expression-injection and java/ognl-injection. For manual and dynamic testing, the OWASP Web Security Testing Guide has test cases for NoSQL, LDAP and XPath injection. The fix cards at /fix/nosql-injection/ and /fix/ldap-injection/ have tested versions of the type check and the filter escaping.
| Input | Vulnerable code | Fixed code |
|---|---|---|
code: {"$ne": null} | 200, invite redeemed | 400 |
code: {"$regex": "^Z"} | 200 or 404 depending on the prefix | 400 |
uid: * | Every person | No entries |
uid: alice)(departmentNumber=e* | Match only if the department starts with e | No entries |
| password: empty string | Depends on the server's unauthenticated-bind setting | false, no bind attempted |
sku: x' or '1'='1 | Every product, including internal ones | No products |
rule: T(java.lang.System).getProperty(...) | The static method runs | SpelEvaluationException, TYPE_NOT_FOUND |
// redeem.test.mjs: run with `node --test` (mongodb, mongodb-memory-server, express)
import test from 'node:test';
import assert from 'node:assert/strict';
import express from 'express';
import { MongoClient } from 'mongodb';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { mountRedeem } from './invites-fixed.mjs';
test('invite codes are matched as literal strings', async (t) => {
const mongod = await MongoMemoryServer.create();
const client = await MongoClient.connect(mongod.getUri());
t.after(async () => { await client.close(); await mongod.stop(); });
const invites = client.db('test').collection('invites');
await invites.insertOne({ code: 'K7Q2-PLUM', team: 'red', used: false });
const app = express();
mountRedeem(app, invites);
const server = await new Promise((ok) => { const s = app.listen(0, () => ok(s)); });
t.after(() => server.close());
const redeem = (code) => fetch(`http://127.0.0.1:${server.address().port}/invites/redeem`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ code }),
}).then((r) => r.status);
assert.equal(await redeem({ $ne: null }), 400); // operator object
assert.equal(await redeem({ $regex: '^K' }), 400); // blind extraction probe
assert.equal(await redeem(['K7Q2-PLUM']), 400); // array instead of string
assert.equal(await redeem('K7Q2-PLUM'), 200); // the real code works once
assert.equal(await redeem('K7Q2-PLUM'), 404); // and only once
});Guided practice
Review a support portal that searches tickets in MongoDB, checks staff logins against LDAP, reads an XML price list and sorts results with SpEL. Decide what each input does before and after the fix.
Supplied scenario material
Route A (Express 5, express.json()):
tickets.find({ customerEmail: req.body.email, status: req.body.status })
Route B (python-ldap):
filter = "(&(objectClass=inetOrgPerson)(uid=" + uid + "))"; then bind as the entry's DN
Route C (lxml):
doc.xpath("//price[@region='" + region + "']")
Route D (Spring):
parser.parseExpression(request.getParameter("sort")).getValue(new StandardEvaluationContext(ticket))
Inputs:
A1 {"email": "ann@example.com", "status": "open"}
A2 {"email": {"$ne": null}, "status": "open"}
A3 {"email": "ann@example.com", "status": {"$in": ["open", "closed"]}}
B1 uid "ann", password "correct-horse"
B2 uid "*", password "anything"
B3 uid "ann", password ""
C1 region "eu"
C2 region "eu' or '1'='1"
D1 sort "priority"
D2 sort "T(java.lang.Runtime).getRuntime().availableProcessors()"1. Predict Route A
State which tickets A2 and A3 return with the current code, and why A3 is still a problem.
Expected result and reasoning
A2 returns open tickets for every customer, because
{$ne: null}matches any email. A3 widens the status filter to closed tickets; even if that looks harmless, the caller has chosen the operator. Both must be rejected because email and status must be strings.2. Repair Route A
Write the validation and the filter for the fixed route.
Expected result and reasoning
Require
typeof email === 'string'with an email pattern andstatusin an allowlist such asopen,pendingandclosed. Query{ customerEmail: { $eq: email }, status: { $eq: status } }and add the caller's customer ID from the session, because another customer's valid email is an authorization problem, not an injection.3. Repair Route B
Rewrite the filter with python-ldap and decide B1, B2 and B3.
Expected result and reasoning
Use
ldap.filter.filter_formatwith[uid]. B1 binds as Ann's DN and succeeds. B2 becomesuid=\2a, matches nobody and fails. B3 is refused before any bind because the password is empty.4. Repair Route C
Rewrite the XPath call and evaluate C1 and C2.
Expected result and reasoning
doc.xpath("//price[@region=$region]", region=region). C1 returns the EU prices. C2 is a literal string that matches no price, and the expression text never changes.5. Remove Route D's interpreter
Replace the SpEL call with a design that keeps D1 working.
Expected result and reasoning
Map allowed sort keys to comparators in code, for example
priorityandcreated. D1 works; D2 is not a key and gets a 400. SimpleEvaluationContext would also reject D2 withTYPE_NOT_FOUND, but a map removes the interpreter entirely.
The fixture is complete, so this works on paper. To run it, adapt the tested snippets in this guide, which use mongodb-memory-server, an OpenLDAP test container, lxml and spring-expression.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Repair the code
SSTI lab: Render a welcome banner without template injection
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP Cheat Sheet Series: NoSQL Security
- OWASP Cheat Sheet Series: LDAP Injection Prevention
- RFC 4515, section 3: escaping in LDAP search filter strings. June 2006.
- RFC 4513, section 5.1.2: unauthenticated simple bind. June 2006.
- MongoDB Manual: server-side JavaScript and how to disable it
- Mongoose API: the sanitizeFilter option. Mongoose 9.11.1.
- Express: migrating to Express 5 (query parser and urlencoded defaults)
- lxml: XPath variables
- Spring Framework reference: SpEL evaluation contexts
- Apache Struts: security guide (OGNL)
Reference information
- SDLC phase
- Development
- CAPEC attack patterns