GHSA-fw9q-39r9-c252: LangSmith Client SDKs has Prototype Pollution in langsmith-sdk via Incomplete `__proto__` Guard in Internal lodash `set()`
GHSA-fw9q-39r9-c252: Prototype Pollution via Incomplete Lodash `set()` Guard in `langsmith-sdk`**Severity:** Medium (CVSS ~5.6)
**Status:** Fixed in 0.5.18SummaryThe 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.Affected Products| Product | Affected Versions | Component |
|---------|-------------------|-----------|
| `langsmith` (npm) | <= 0.5.17 | `js/src/utils/lodash/baseAssignValue.ts`, `js/src/anonymizer/index.ts` |
| langchain-ai/langsmith-sdk | GitHub main branch (as of 2026-03-24) | JS/TypeScript SDK |**Not affected:** The Python SDK (`langsmith` on PyPI) does not use lodash or an equivalent pattern.Root CauseThe 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:This blocks `__proto__` pollution but does **not** block the `constructor.prototype` traversal path. When `set()` is called with a path like `"constructor.prototype.polluted"`:`castPath()` splits it into `["constructor", "prototype", "polluted"]``baseSet()` iterates: `obj.constructor` → `Object` → `Object.prototype``assignValue(Object.prototype, "polluted", value)` calls `baseAssignValue()`Key is `"polluted"` (not `"__proto__"`), so the guard is bypassed`Object.prototype.polluted = value` — all objects are pollutedAttack Vector via AnonymizerThe `createAnonymizer()` API (importable as `langsmith/anonymizer`) processes data by:**Extracting string nodes** — `extractStringNodes()` walks an object recursively and builds dotted paths from keys**Applying regex replacements** — If a string value matches a configured pattern, the node is marked for update (`anonymizer/index.ts:95`)**Writing back with `set()`** — `set(mutateValue, node.path, node.value)` writes the replaced value back (`anonymizer/index.ts:123`)An attacker who controls keys in data being anonymized can construct a nested object where the path resolves to `constructor.prototype.X`:`extractStringNodes()` produces path `"wrapper.constructor.prototype.isAdmin"`. When the replacement triggers and `set()` writes back, it traverses up to `Object.prototype`.Although `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.Proof of ConceptImpactPrototype pollution in a Node.js process can enable:**Authentication bypass** — `if (user.isAdmin)` checks succeed on all objects**Remote Code Execution** — Exploitable in template engines (Pug, EJS, Handlebars, Nunjucks) via polluted prototype properties that reach `eval()`/`Function()` sinks**Denial of Service** — Overwriting `toString`, `valueOf`, or `hasOwnProperty` on all objects**Data exfiltration** — Polluting serialization methods to inject attacker-controlled valuesRemediationIn `baseAssignValue.ts`, extend the guard to cover `constructor` and `prototype` keys:As defense in depth, `extractStringNodes()` in `anonymizer/index.ts` should also sanitize or reject path segments matching `constructor` or `prototype` before passing them to `set()`.Timeline| Date | Event |
|------|-------|
| 2026-03-24 | Initial report submitted |
| 2026-04-09 | Vendor confirmed; fixed in 0.5.18 |CreditsReported by: OneThing4101
CVSS v3.1: 5.6