# Three.js — CWE-407 Disclosure Brief
**2026-03-27 · Patch available — awaiting upstream merge**

## Finding

Five O(n²) defects in Three.js's WebGL uniform binding allocator, shader node graph, and node builder systems. All patched. Patches ready for upstream review. Three.js is the dominant JavaScript 3D library (~100k GitHub stars) used in web applications, data visualizations, and games.

## The Defects

**threejs-0001 (PATCHED — HIGH):** `src/renderers/webgl/WebGLUniformsGroups.js`

```javascript
// In allocateBindingPointIndex() — per uniform group per frame:
for (let i = 0; i < maxBindingPoints; i++) {
    if (allocatedBindingPoints.indexOf(i) === -1) {  // O(n) per iteration
        allocatedBindingPoints.push(i);
        return i;
    }
}
// allocatedBindingPoints is Array — indexOf is O(n) inside O(maxBindingPoints) loop
```

**22× op reduction** with `Set` shadow.

**threejs-0002 (PATCHED — HIGH):** `src/nodes/core/StackNode.js`

```javascript
// In StackNode.build() — shader node dedup:
const filteredNodes = nodes.filter(node =>
    nodes.indexOf(node) === -1  // O(n) per node in O(n) filter
);
// O(n²) to filter duplicates from shader node list
```

**1,875× op reduction** with `const nodesSet = new Set(nodes)` before filter.

**threejs-0003 (PATCHED — HIGH):** `src/nodes/core/NodeBuilder.js:693`

```javascript
// In getBindingGroups() — triple-nested loop:
for (const stage of stages) {
    for (const group of groups) {
        for (const uniform of uniforms) {
            if (groupUniforms.includes(uniform)) { ... }  // O(n) per uniform
        }
    }
}
// groupUniforms is Array — includes() is O(n) in O(stages × groups × uniforms)
```

**threejs-0004 (PATCHED — HIGH):** `src/nodes/core/NodeBuilder.js:763`

```javascript
// In addNode() — called on every node addition:
if (!this.nodes.includes(node)) {  // O(n) per add — Array.includes()
    this.nodes.push(node);
}
```

**threejs-0005 (PATCHED — HIGH):** `src/nodes/core/NodeBuilder.js:787`

```javascript
// In addSequentialNode() — called on every sequential node add:
if (!this.sequentialNodes.includes(node)) {  // O(n) per add — Array.includes()
    this.sequentialNodes.push(node);
}
```

## Complexity Proof

**threejs-0001:** For maxBindingPoints B and already-allocated A slots:
- Worst case: O(B × A) scans to find free slot
- **22× op reduction** with Set shadow at B=32, A=28.

**threejs-0002:** For N nodes in the shader graph:
- `nodes.indexOf()` O(N) inside `filter` over N nodes: **O(N²)**
- At N=250 nodes: **1,875× op reduction.**

**threejs-0003:** Triple-nested loop with O(uniforms) `includes()` per iteration:
- **517× combined op reduction** across threejs-0003/0004/0005 at typical shader complexity.

**threejs-0004/threejs-0005:** For N nodes added to the builder:
- Per `addNode()` call: O(N) `includes()` scan
- Total N calls: **O(N²)**

## Impact

Three.js is used in thousands of production web applications: interactive data visualizations, WebXR experiences, product configurators, architectural previews, and browser-based games. It is the standard answer to "how do I do 3D in the browser."

threejs-0001 fires on every frame for every WebGL uniform group allocation — the overhead is present in every Three.js render loop. threejs-0002 fires on shader graph compilation — which occurs when materials or shader programs are first compiled (on object creation or material change). threejs-0004/0005 fire on every `addNode()` / `addSequentialNode()` call in the node material system (Three.js's WebGPU-ready node-based shader system).

Applications using the Three.js node material system (WebGPU renderer, custom shader graphs) are particularly affected.

## The Fix

**threejs-0001:** Shadow `Set` for binding point tracking:

```javascript
// Before
if (allocatedBindingPoints.indexOf(i) === -1) { ... }

// After
// CWE-407 fix: Set for O(1) has() instead of O(n) indexOf().
const allocatedBindingPointsSet = new Set();
if (!allocatedBindingPointsSet.has(i)) {
    allocatedBindingPointsSet.add(i);
    // ...
}
```

**threejs-0002:** Hoist `new Set(nodes)` before filter:

```javascript
// Before
nodes.filter(node => nodes.indexOf(node) === -1)

// After
// CWE-407 fix: Set for O(1) has() instead of O(n) indexOf() per filter element.
const nodesSet = new Set(nodes);
nodes.filter(node => !nodesSet.has(node))
```

**threejs-0003:** `Map` of `Set`s for group-uniform tracking:

```javascript
// Before
if (groupUniforms.includes(uniform)) { ... }

// After
// CWE-407 fix: Map<group, Set<uniform>> for O(1) has() instead of O(n) includes().
const groupSets = new Map();
if (!groupSets.get(group)?.has(uniform)) { ... }
```

**threejs-0004/threejs-0005:** Instance-level `Set` shadows:

```javascript
// Before
this.nodes = [];
if (!this.nodes.includes(node)) { this.nodes.push(node); }

// After
// CWE-407 fix: instance Set shadow for O(1) has() instead of O(n) includes().
this.nodes = [];
this.nodesSet = new Set();
if (!this.nodesSet.has(node)) {
    this.nodesSet.add(node);
    this.nodes.push(node);
}
```

## Patch

Fix available: `defects/threejs/patch/threejs-0001-0005-set-shadow-index.patch`

Five-location patch across `WebGLUniformsGroups.js`, `StackNode.js`, and `NodeBuilder.js`.

Unit test: `ThreeJSTest` 6/6 pass. threejs-0001: **22× speedup**. threejs-0002: **1,875× speedup**. threejs-0003/0004/0005: **517× combined speedup**.

## What We Ask

A patch is ready for review.

1. Confirm receipt and assign a GitHub issue reference (mrdoob/three.js).
2. Assess severity — threejs-0002 is the most severe at 1,875×; threejs-0004/0005 fire on every node addition in the node material system.
3. Coordinate a disclosure date — we are targeting 90 days from first contact.
4. We will credit the Three.js team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.
