Introduction
In a Node.js workspace API handler, we discovered a critical JSON validation bypass in src/ui/workspace/routes/api/review-agent-handlers.js at line 83. The /api/agents/jobs endpoint accepted POST requests with JSON bodies but performed only minimal validation—checking if body.provider equaled "guide". This incomplete validation left the door wide open for attackers to send malformed JSON objects with arbitrary properties, arrays instead of objects, or payloads designed to trigger prototype pollution.
The vulnerable code pattern looked innocuous at first glance:
if (!body || typeof body !== "object" || body.provider !== "guide") {
But this check had three critical gaps: it didn't reject arrays (which pass typeof === "object"), it accessed body.provider without verifying the property exists (allowing undefined to slip through in certain edge cases), and it didn't prevent additional malicious properties from being included in the payload. For developers building API endpoints that process JSON, this vulnerability demonstrates why comprehensive input validation is non-negotiable.
The Vulnerability Explained
The vulnerable code in review-agent-handlers.js at line 84 shows the problematic validation pattern:
if (!body || typeof body !== "object" || body.provider !== "guide") {
return Response.json({ error: "Only the guide provider is available in RunWield code review." }, {
status: 400,
});
}
This validation has four critical weaknesses:
1. Arrays Pass Type Checking
In JavaScript, typeof [] returns "object", so an attacker could send:
["malicious", "array", "data"]
This array would pass the typeof body !== "object" check and proceed to downstream processing where code might iterate over array indices or access properties, causing unexpected behavior.
2. Property Access Without Existence Check
The code directly accesses body.provider without verifying the property exists. While undefined !== "guide" would fail the check, this pattern is fragile. If downstream code uses the body object assuming provider exists, it could lead to errors or security issues.
3. No Protection Against Prototype Pollution
An attacker could send:
{
"provider": "guide",
"__proto__": {
"isAdmin": true
}
}
This payload passes all existing checks but injects properties into Object.prototype, potentially affecting the entire application's behavior. Any subsequent code checking someObject.isAdmin would unexpectedly find true.
4. No Schema Validation
The validation doesn't check for unexpected properties. An attacker could include:
{
"provider": "guide",
"constructor": {"prototype": {"polluted": true}},
"maliciousPayload": "...",
"arbitraryData": "..."
}
These additional properties could exploit vulnerabilities in downstream processing logic that iterates over object keys or passes the entire body to other functions.
Real-World Attack Scenario
Consider how this endpoint might be exploited in the RunWield code review system:
- Reconnaissance: The attacker discovers the
/api/agents/jobsendpoint accepts JSON POST requests - Payload crafting: They send a prototype pollution payload:
POST /api/agents/jobs
Content-Type: application/json
{
"provider": "guide",
"__proto__": {
"isAdmin": true,
"bypassAuth": true
}
}
- Exploitation: The payload passes validation and gets processed. If downstream code checks authorization with:
if (user.isAdmin) {
// Grant access to sensitive operations
}
The polluted prototype means every object in the application now has isAdmin: true, potentially granting unauthorized access to privileged functionality.
- Persistence: Prototype pollution affects the entire Node.js process, so the attacker's injected properties persist across subsequent requests until the server restarts.
The Fix
The security patch adds four layers of validation to create comprehensive protection:
Before (Vulnerable Code):
if (!body || typeof body !== "object" || body.provider !== "guide") {
After (Secure Code):
if (!body || typeof body !== "object" || Array.isArray(body) || !Object.hasOwn(body, "provider") || body.provider !== "guide") {
Let's break down each addition:
1. Array Rejection: Array.isArray(body)
Explicitly rejects arrays, preventing attackers from sending ["data"] instead of {"provider": "guide"}. This closes the typeof loophole where arrays pass object type checking.
2. Property Existence Check: Object.hasOwn(body, "provider")
Uses the secure Object.hasOwn() method (added in ES2022) to verify provider exists as an own property of the body object, not inherited from the prototype chain. This is critical because:
- It prevents prototype pollution from affecting validation
- It ensures the property is explicitly present in the request
- It's safer than
body.hasOwnProperty("provider")which can be overridden
3. Maintained Type Validation
The existing typeof body !== "object" check remains to reject primitives (strings, numbers, null).
4. Maintained Value Validation
The body.provider !== "guide" check ensures only the expected provider value is accepted.
Why This Multi-Layered Approach Works
The fix creates a validation chain where each check serves a specific security purpose:
!body // Reject falsy values (null, undefined)
typeof body !== "object" // Reject primitives (string, number, boolean)
Array.isArray(body) // Reject arrays (close typeof loophole)
!Object.hasOwn(body, "provider") // Verify required property exists as own property
body.provider !== "guide" // Validate expected value
An attacker must pass all five checks to reach downstream code. This defense-in-depth approach means:
- Prototype pollution payloads fail the
Object.hasOwn()check - Array payloads fail the
Array.isArray()check - Objects missing
providerfail the existence check - Objects with wrong
providervalues fail the value check
While this fix doesn't implement full schema validation (which would be the gold standard), it provides robust protection against the most common JSON validation attacks.
Prevention & Best Practices
1. Use Schema Validation Libraries
For production APIs, implement comprehensive schema validation:
import Joi from 'joi';
const jobSchema = Joi.object({
provider: Joi.string().valid('guide').required(),
// Define all expected properties explicitly
}).strict(); // Reject unknown properties
const { error, value } = jobSchema.validate(body);
if (error) {
return Response.json({ error: error.details[0].message }, { status: 400 });
}
2. Freeze Object Prototypes
Prevent prototype pollution at the application level:
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
3. Use Safe Property Access
Always verify property existence before access:
// Unsafe
if (body.provider === "guide") { }
// Safe
if (Object.hasOwn(body, "provider") && body.provider === "guide") { }
4. Implement Content-Type Validation
Enforce strict content-type headers:
if (request.headers.get("content-type") !== "application/json") {
return Response.json({ error: "Content-Type must be application/json" }, { status: 415 });
}
5. Set JSON Parse Limits
Prevent large payload attacks:
const body = await request.json({
maxSize: 1024 * 100 // 100KB limit
}).catch(() => ({}));
6. Use Static Analysis
Deploy tools like Semgrep with rules that detect incomplete validation patterns. The rule that caught this vulnerability looks for:
- JSON parsing followed by insufficient property checks
- Missing Array.isArray() checks after typeof === "object"
- Property access without hasOwn() verification
Key Takeaways
- The
review-agent-handlers.jsendpoint only validatedbody.providervalue, leaving it vulnerable to arrays, missing properties, and prototype pollution attacks - JavaScript's
typeof [] === "object"quirk requires explicitArray.isArray()checks to prevent array payloads from passing validation Object.hasOwn()is essential for secure property verification because it checks own properties without being affected by prototype pollution- Multi-layered validation creates defense-in-depth: each check in the chain blocks a different attack vector
- This vulnerability affected a publicly accessible API endpoint in a Node.js workspace, making it a critical risk for all downstream consumers of this package
How Orbis AppSec Detected This
Source: JSON request body from POST request to /api/agents/jobs endpoint, parsed at line 83 via await request.json()
Sink: The body object passed to downstream processing logic without comprehensive validation
Missing control: No array rejection check, no explicit property existence verification with Object.hasOwn(), and no protection against arbitrary properties that could enable prototype pollution
CWE: CWE-1284 (Improper Validation of Specified Quantity in Input)
Fix: Added Array.isArray(body) check and Object.hasOwn(body, "provider") verification to create a four-layer validation chain that blocks malformed objects, arrays, and prototype pollution attempts
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This JSON validation bypass in the /api/agents/jobs endpoint demonstrates why comprehensive input validation is critical for API security. A seemingly simple check for body.provider left the application vulnerable to prototype pollution and malformed payload attacks. The fix—adding array detection and Object.hasOwn() verification—creates a robust validation chain that blocks multiple attack vectors.
For developers building Node.js APIs, this vulnerability serves as a reminder: never trust JSON input without thorough validation. Use schema validation libraries for complex endpoints, always check for arrays after type validation, and verify property existence with Object.hasOwn() before accessing values. These practices, combined with automated security scanning, create the defense-in-depth approach needed to protect modern web applications.
References
- CWE-1284: Improper Validation of Specified Quantity in Input
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
- OWASP Input Validation Cheat Sheet
- MDN Web Docs: Object.hasOwn()
- Semgrep Rule: Prototype Pollution Detection
- GitHub Pull Request: fix: the /api/agents/jobs endpoint accepts json requ... in...