GitHub Security Advisory: GHSA-fw9q-39r9-c252
Package: langsmith (NPM)
Severity: MODERATE (CVSS 5.6)
CWE: CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Vulnerable versions: <= 0.5.17
Patched in: 0.5.18

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`

**Severity:** Medium (CVSS ~5.6)
**Status:** Fixed in 0.5.18

---

## Summary

The 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 Cause

The 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:

```typescript
function baseAssignValue(object: Record<string, any>, key: string, value: any) {
  if (key === "__proto__") {
    Object.defineProperty(object, key, {
      configurable: true, enumerable: true, value: value, writable: true,
    });
  } else {
    object[key] = value;  // ← No guard for "constructor" or "prototype" keys
  }
}
```

This blocks `__proto__` pollution but does **not** block the `constructor.prototype` traversal path. When `set()` is called with a path like `"constructor.prototype.polluted"`:

1. `castPath()` splits it into `["constructor", "prototype", "polluted"]`
2. `baseSet()` iterates: `obj.constructor` → `Object` → `Object.prototype`
3. `assignValue(Object.prototype, "polluted", value)` calls `baseAssignValue()`
4. Key is `"polluted"` (not `"__proto__"`), so the guard is bypassed
5. `Object.prototype.polluted = value` — all objects are polluted

---

## Attack Vector via Anonymizer

The `createAnonymizer()` API (importable as `langsmith/anonymizer`) processes data by:

1. **Extracting string nodes** — `extractStringNodes()` walks an object recursively and builds dotted paths from keys
2. **Applying regex replacements** — If a string value matches a configured pattern, the node is marked for update (`anonymizer/index.ts:95`)
3. **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`:

```javascript
{
  wrapper: {
    "constructor.prototype.isAdmin": "contains-secret-pattern"
  }
}
```

`extractStringNodes()` produces path `"wrapper.constructor.prototype.isAdmin"`.