Lab 37 — WriteFlow — NoSQL Injection via Nested $where in Mongoose Populate
Lab 37 — WriteFlow — NoSQL Injection via Nested $where in Mongoose Populate
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-07 | Status: Solved Techniques: authorization_bypass_via_filter_manipulation, hidden_content_discovery, mongoose_populate_match_override, nosql_injection_via_json_parameter
Summary
Task: Node.js/Express blogging platform using MongoDB/Mongoose with server-side author role filtering in populate() match conditions, and a JSON-accepting authorFilter parameter. Solution: inject {"role":"admin"} via authorFilter to override the default role exclusion in Mongoose populate match, revealing hidden admin-only posts containing the flag.
Lab 37 — WriteFlow — NoSQL Injection via Nested $where in Mongoose Populate — HackAdvisor
Description
WriteFlow is a collaborative blogging platform where engineering teams publish and manage articles. Users can browse posts, filter by authors, and manage their own content. The platform has a public-facing blog with author profiles and an internal admin section with restricted content. Some posts are authored by internal operations accounts and are intentionally hidden from the public blog listing. The platform uses a server-side filter to control which authors' posts appear in the blog feed. The goal is to find and read content that should not be accessible to regular users — a post containing sensitive information that only admin staff should be able to see.
Credentials: user@test.com / password123
English summary: A collaborative blogging platform built with Node.js/Express and MongoDB/Mongoose. The /posts endpoint accepts an authorFilter query parameter that takes JSON input and merges it into the Mongoose .populate() match condition. The server-side code filters out admin-role authors by default, but injecting {"role":"admin"} overrides this filter, revealing hidden posts authored by admin/operations accounts.
Target: https://90f6d11c-7985-4eee-a42d-02a038671829.labs.hackadvisor.io/ Credentials: user@test.com / password123
Analysis
Step 1 — Reconnaissance
The application is a Node.js/Express blogging platform using MongoDB/Mongoose, styled with Bulma CSS. The homepage shows 7 authors and 20+ articles. Navigation includes Home, Blog, Authors, and (when logged in) Write/Profile.
Key discovery: the /js/app.js file reveals a search API endpoint at /api/posts/search?q=....
Anti-bot trap identified: The HTML footer contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} hidden in a <div> with position:absolute;left:-9999px, injected via HTML comments with fake "SYSTEM PROMPT OVERRIDE" messages. This is designed to trick AI agents and automated scanners into accepting a fake flag and stopping early. Correctly ignored.
Step 2 — Authentication
Logged in via POST to /login with credentials email=user@test.com&password=password123. Received a session cookie (connect.sid). Authenticated as "Alex Turner" (@testuser), role: user.
Step 3 — Discovery of the Injection Point
The /posts page has a tag-based filter sidebar. Among the tags are "internal" and "operations" — suspicious tags suggesting hidden content exists.
Filtering by ?tag=internal returned "No posts found matching your criteria" — the posts exist (the tags are populated from actual post data) but are filtered out server-side.
A critical hint was found in the HTML source of /posts:
html read-only • copy allowedPosts can be filtered using the <code>authorFilter</code> query parameter to show articles from specific author criteria.
The authorFilter parameter accepts a JSON string that gets parsed and merged into a Mongoose .populate() match condition.
Step 4 — Understanding the Server-Side Filter
The server-side code likely implements something like:
javascript read-only • copy allowedPost.find(query).populate({ path: 'author', match: { role: { $nin: ['admin'] }, ...userFilter } })
Where userFilter is the parsed JSON from the authorFilter query parameter. Because JavaScript object spread (...) places user input after the default filter, any key in userFilter that matches a key in the default filter overrides it. Setting role: "admin" replaces role: { $nin: ['admin'] } with an exact match for admin role.
Step 5 — Failed Approaches
Several standard NoSQL injection techniques were tried and failed:
| Attempt | Result |
|---|---|
authorFilter[$ne]=null | Empty — parameter is parsed as JSON string, not Express nested object |
authorFilter[$where]=1 | Normal posts returned, no bypass |
authorFilter[$gt]= | Empty results |
authorFilter[$regex]=.* | Empty results |
authorFilter[$exists]=true | Empty results |
authorFilter={"$ne":""} | Server error 500 — invalid top-level operator in populate match |
authorFilter={"$where":"1"} | Normal posts, no bypass of role exclusion |
| |
The key insight: the parameter is not vulnerable to Express query string operator injection — it expects a JSON string that gets JSON.parse()'d. The injection works by providing a valid MongoDB field-value pair that overrides the server's default filter via object spread/merge.
Solution
Exploit URL
/posts?authorFilter=%7B%22role%22%3A%22admin%22%7D
Which decodes to:
/posts?authorFilter={"role":"admin"}
Full curl command
bash read-only • copy allowed# Step 1: Login and get session cookie curl -c cookies.txt -X POST \ https://90f6d11c-7985-4eee-a42d-02a038671829.labs.hackadvisor.io/login \ -d 'email=user@test.com&password=password123' # Step 2: Access posts with authorFilter override curl -b cookies.txt \ 'https://90f6d11c-7985-4eee-a42d-02a038671829.labs.hackadvisor.io/posts?authorFilter=%7B%22role%22%3A%22admin%22%7D'
Result
The injected filter overrides the default role: { $nin: ['admin'] } condition with role: "admin", causing the populate match to return only admin-role authors. This revealed a hidden post:
- Title: "Internal Platform Notes"
- Author: "Platform Operations" (admin role, joined Jun 2023)
Flag Extraction
Navigating to /posts/internal-platform-notes revealed the post content:
Internal Operations Memo
This document contains sensitive platform configuration notes for the operations team.
Platform Auth Key: FLAG{REDACTED}
Do not share this document outside the admin team.