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, 2026•Reviewed 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

high

linkify-it 5.0.1 mailto: Link Parsing Causes DoS

linkify-it versions up to 5.0.1 can be forced into excessive processing time when autolinking a specially crafted mailto: link, allowing a remote attacker to degrade or stall the parsing thread. Upgrading to linkify-it 5.0.2 closes the issue; any application that runs linkify-it (directly or via markdown-it) against untrusted text should update immediately.

critical

Slim CLI Unverified Remote Fetch in Version Check

The Slim CLI's version check command fetched remote package metadata without integrity verification, enabling attackers to serve malicious responses through repository hijacking or man-in-the-middle attacks. The fix adds input validation, HTTP status checking, and response schema verification to ensure only legitimate version data is processed.

critical

ensureTrivy() CWE-494: Unverified Trivy Binary Download

The `ensureTrivy()` function fetched the Trivy vulnerability scanner from GitHub releases without verifying its integrity, exposing applications to supply-chain attacks. An attacker in a MITM position or a compromised CDN could substitute a malicious binary that executes with the application's privileges. The fix adds cryptographic verification using SHA256 checksums published alongside each release.

high

@xmldom/xmldom 0.9.10 ReDoS: CVE-2026-83606 in PI Parsing

The dependency tree pinned `@xmldom/xmldom` at 0.9.10, a version vulnerable to CVE-2026-83606: a regular expression in the processing-instruction (`<?target data?>`) parsing path backtracks catastrophically on crafted input, so a single XML document can consume CPU for seconds or minutes. Because Node.js runs application code on one thread, that stall blocks every other request in flight. The lockfile was updated to resolve `@xmldom/xmldom` 0.9.12, which carries the fix.

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li

high

js-yaml 4.3.1 Denial of Service: Malformed Input Hangs YAML Parser

A denial of service vulnerability in js-yaml versions 4.3.1 and earlier allows attackers to hang the YAML parser indefinitely by providing specially crafted malformed input. The fix, released in versions 4.3.2 and 3.15.2, patches the parsing logic to prevent unbounded processing. Upgrading is recommended for all applications parsing untrusted YAML data.