Back to Blog
critical SEVERITY6 min read

How Insufficient Input Validation happens in TypeScript and how to fix it

A critical input validation vulnerability was discovered in `src/mcp/presets/commerce/inputs.ts` where the `normalizeCommerceAccountInput` function accepted loosely-typed `Record<string, any>` arguments without verifying field types or object structure. This allowed attackers to inject malicious payloads through MCP tool invocations. The fix adds explicit type guards and structural validation to ensure only properly-typed string values reach downstream consumers.

O
By Orbis AppSec
Published August 15, 2026Reviewed August 15, 2026

Answer Summary

This is an Insufficient Input Validation vulnerability (CWE-20) in a TypeScript Node.js library where the `normalizeCommerceAccountInput` function accepted `Record<string, any>` without type checking individual fields. Attackers could pass non-string values (objects, arrays, or prototype-polluting payloads) through MCP tool endpoints. The fix adds explicit `typeof` checks for each field and validates the `platform` argument is a plain object before accessing its properties.

Vulnerability at a Glance

cweCWE-20 (Improper Input Validation)
fixAdded structural type guard (`typeof p === 'object' && !Array.isArray(p)`) and per-field `typeof` string checks
riskAttackers can inject malicious objects, trigger prototype pollution, or pass unexpected types through MCP commerce endpoints
languageTypeScript / Node.js
root cause`normalizeCommerceAccountInput` trusts `args.platform` without verifying it is a plain object or that its properties are strings
vulnerabilityInsufficient Input Validation / Improper Input Neutralization

Introduction

The file src/mcp/presets/commerce/inputs.ts is responsible for normalizing raw arguments from MCP (Model Context Protocol) tool invocations into typed commerce account structures. At line 16, the normalizeCommerceAccountInput function accepted a Record<string, any> argument and immediately accessed nested properties of args.platform without any structural or type validation. This meant any caller—including untrusted external input from MCP endpoints—could pass arbitrary objects, arrays, or prototype-polluting payloads directly into the platform account fields.

Because this is a Node.js library consumed by downstream packages, the vulnerability extends beyond the immediate codebase. Every application that invokes normalizeCommerceAccountInput with user-controlled data inherits this weakness.

The Vulnerability Explained

The Dangerous Pattern

Here's the vulnerable code before the fix:

export function normalizeCommerceAccountInput(args: Record<string, any>): CommerceAccountInput {
  const platformAccounts = args.platform
    ? [
        {
          platformUrl: args.platform.url,
          username: args.platform.username,
          password: args.platform.password,
          twoFactorKey: args.platform.twoFactorKey,
          remarks: args.platform.remarks,
        },
      ]
    : undefined;

The problems are layered:

  1. No structural check on args.platform: The code uses a simple truthy check (args.platform). Any truthy value—an array, a function, a string—passes this guard. An attacker could pass platform: "__proto__" or platform: [malicious_array] and the code would attempt to read properties from it.

  2. No type validation on nested fields: args.platform.url, args.platform.username, etc. are assigned directly without verifying they are strings. An attacker could supply { url: { toString: () => "http://evil.com" } } or inject objects that exploit downstream serialization.

  3. Prototype pollution vector: Without checking !Array.isArray(p) and typeof p === 'object', an attacker could craft payloads that exploit JavaScript's prototype chain when the resulting object is spread or merged downstream.

Exploitation Scenario

Consider an attacker invoking an MCP tool that calls normalizeCommerceAccountInput:

{
  "platform": {
    "url": {"__proto__": {"isAdmin": true}},
    "username": ["injected", "array"],
    "password": {"toString": "() => process.env.SECRET"},
    "remarks": 12345
  }
}

Because the original code performs no type checking:
- platformUrl receives an object instead of a string, potentially causing prototype pollution when merged with other objects
- username receives an array, which could break downstream logic expecting .trim() or .toLowerCase()
- password receives an object with a crafted toString, which could leak secrets if coerced to string in logging
- remarks receives a number, bypassing any string-length or pattern validation downstream

In a production environment where MCP tools are exposed over HTTP or WebSocket, this is directly exploitable by any client that can invoke the commerce account tool.

The Fix

The fix introduces two layers of defense:

1. Structural Validation of args.platform

const p = args.platform;
const platformAccounts = p != null && typeof p === 'object' && !Array.isArray(p)

This ensures p is:
- Not null or undefined (p != null)
- Actually an object (typeof p === 'object')
- Not an array (!Array.isArray(p))

This eliminates the entire class of attacks where platform is a string, number, array, or null.

2. Per-Field Type Guards

{
  platformUrl: typeof p.url === 'string' ? p.url : undefined,
  username: typeof p.username === 'string' ? p.username : undefined,
  password: typeof p.password === 'string' ? p.password : undefined,
  twoFactorKey: typeof p.twoFactorKey === 'string' ? p.twoFactorKey : undefined,
  remarks: typeof p.remarks === 'string' ? p.remarks : undefined,
}

Each field is individually validated. If an attacker supplies a non-string value for any field, it falls back to undefined rather than propagating the malicious value. This is a defense-in-depth approach: even if the structural check were somehow bypassed, individual fields are still protected.

Before vs. After

Aspect Before After
Platform check Truthy check only typeof === 'object' + !Array.isArray
Field validation None typeof === 'string' per field
Array input Passes through Rejected (returns undefined)
Object injection Passes through Rejected (returns undefined)
Valid input behavior Unchanged Unchanged

Key Takeaways

  • Record<string, any> is not a validation mechanism—it's a type annotation that vanishes at runtime. The normalizeCommerceAccountInput function needed runtime guards, not just TypeScript types.
  • Truthy checks (if (args.platform)) are insufficient structural validation—strings, numbers, and arrays are all truthy and would pass through to property access.
  • Per-field typeof checks provide defense-in-depth—even when the outer structural guard is correct, validating each field independently prevents partial injection attacks.
  • MCP tool inputs are untrusted by default—any function exposed through MCP endpoints must treat its arguments as potentially malicious, regardless of what TypeScript interfaces suggest.
  • The fix preserves valid behavior—legitimate callers passing proper string fields see no change, demonstrating that security hardening doesn't require breaking changes.

How Orbis AppSec Detected This

  • Source: Untrusted input entering via MCP tool invocations as Record<string, any> arguments to normalizeCommerceAccountInput
  • Sink: Direct property access on args.platform.url, args.platform.username, etc. at src/mcp/presets/commerce/inputs.ts:16 without type validation
  • Missing control: No runtime type guards on the platform object structure or its individual string fields; no schema validation library applied
  • CWE: CWE-20 (Improper Input Validation)
  • Fix: Added structural type guard (typeof p === 'object' && !Array.isArray(p)) and per-field typeof === 'string' checks to reject non-string values

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 vulnerability demonstrates a common but dangerous pattern in TypeScript applications: relying on compile-time types to protect runtime behavior. The normalizeCommerceAccountInput function's Record<string, any> parameter type provided zero runtime protection against malicious payloads. By adding explicit structural validation and per-field type guards, the fix ensures that only properly-typed data reaches downstream consumers—without breaking any existing valid usage.

For any function that sits at a trust boundary (API handlers, MCP tools, webhook processors), always validate at runtime. TypeScript types are documentation for developers; they are not security controls.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.