LearnNewsExamplesServices
Frontmatter
id17494
titleThe docs namespace walker consults inherited properties
stateClosed
labels
bugaibuildsecurity
assigneesneo-opus-grace
createdAtAug 21, 2026, 8:46 PM
updatedAtAug 22, 2026, 1:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/17494
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 22, 2026, 1:17 AM

The docs namespace walker consults inherited properties

Closed Backlog/active-chunk-18 bugaibuildsecurity
neo-opus-grace
neo-opus-grace commented on Aug 21, 2026, 8:46 PM

Context

CodeQL alert 113, js/prototype-pollution-utility, at buildScripts/docs/generateDocsJson.mjs:91.

This is not one of the two alerts @tobiu described as deliberate. That exemption covers src/Neo.mjs:563,565, where the framework enriches prototypes on purpose. This is a third instance of the same rule in a different file — a docs generator with no intent to touch prototypes anywhere — and it surfaced only because I re-read the live alert queue instead of working from a carried list.

Two items I had been carrying turned out to be stale in the same pass, worth recording so nobody re-opens them: the js/cors-permissive-configuration alert from #15818's list is no longer open, and Ada's parked ai/daemons/wake/queries.mjs:84 fixed-temp-path defect is already remediatedwriteFileAtomicSync now uses ${absolute}.${pid}.${randomUUID()}.tmp with flag: 'wx' and a finally { rmSync }, verified at source rather than from its comment.

Live latest-open sweep: checked latest 8 open issues at 2026-08-21T18:5x UTC; gh search issues for setNamespace OR generateDocsJson OR prototype-pollution returned zero. No equivalent exists.

The Problem

buildScripts/docs/generateDocsJson.mjs:80-93:

function setNamespace(tree, names, value) {
    names = Array.isArray(names) ? names : names.split('.');
    let current = tree;

    for (let i = 0; i < names.length - 1; i++) {
        if (!current[names[i]]) {
            current[names[i]] = {};
        }
        current = current[names[i]];
    }

    current[names[names.length - 1]] = value;
}

if (!current[names[i]]) is a truthiness test against inherited properties, not an own-property test. For any segment naming something on Object.prototype, the guard reads the inherited value as "already present", skips creating a fresh object, and walks into the global.

Measured, all three:

namespace segment result
a.constructor.b no own property created; value landed on Object.prototype.constructor.b
toString.x Object.prototype.toString.x mutated
__proto__.polluted ({}).polluted — every object in the process

Severity, stated honestly: latent, not live. I enumerated every namespace segment derivable from src/** and none collides with constructor, __proto__, prototype, toString, valueOf, or hasOwnProperty. Nothing is broken today.

What makes it worth fixing anyway is the direction of the failure, not a severity adjective. A colliding segment does not throw — it writes to a global and silently omits the node from the docs tree. The first symptom would be a missing docs entry or an inexplicable global mutation during a build, with nothing pointing at this function. The two prior code-scanning items in this queue had the same profile: the loud half was harmless and the silent half was the one that mattered.

The __proto__ row is the only one that needs adversarial input; constructor and toString need nothing but an unlucky class name.

The Architectural Reality

  • buildScripts/docs/generateDocsJson.mjs:80-93setNamespace, the sole defective function
  • :338, :347, :351, :361 — its four call sites, all building namespace trees from source-derived paths

The tree is consumed as JSON downstream, so the fix must not change the serialized shape.

Distinct from src/Neo.mjs:563,565 (alerts 62/63): those write to prototypes deliberately and want a documented dismissal, not a code change. Conflating the three under one rule id is what would produce the wrong disposition here.

The Fix

Two small changes to the loop:

  1. Own-property testObject.hasOwn(current, key) instead of truthiness, so an inherited property never masquerades as an existing node.
  2. Reject the pollution keys loudly__proto__, constructor, prototype. A Neo namespace segment is never legitimately one of these, so a throw naming the offending path is strictly better than silent traversal; it surfaces the authoring error at build time instead of yielding a quietly wrong tree.

Failing loud on an impossible input matches the direction #16965 established for the mailbox index contract.

Acceptance Criteria

  • setNamespace uses an own-property test; an inherited property name creates a real node rather than walking into the global
  • __proto__, constructor, prototype segments throw, and the message names the offending namespace path
  • RED-first, one arm per mechanism: a constructor segment must not reach Object.prototype.constructor, and a __proto__ segment must not set ({}).polluted — these fail for different reasons and neither substitutes for the other
  • A control asserts an ordinary nested namespace still builds identically, so the guard cannot pass by rejecting everything
  • The emitted JSON shape is unchanged for existing input — asserted against a real generated tree, not a hand-built fixture
  • CodeQL alert 113 closes on the merge commit

Out of Scope

  • Alerts 62/63 (src/Neo.mjs:563,565) — deliberate prototype enrichment; these want a documented dismissal decision from @tobiu, not a code change, and are not an agent's call to make against the security tab.
  • Any restructuring of the docs tree or its consumers.

Avoided Traps

Treating this as the same finding as 62/63 because the rule id matches. It is the same rule over a different intent: one is deliberate enrichment, this is an accidental walk. A single disposition applied to all three would either dismiss a real defect or "fix" intentional framework behaviour.

Fixing only __proto__. It is the famous key and the least likely to occur here. constructor and toString collide without any adversarial input, and a guard that lists only __proto__ leaves the reachable cases live while closing the alert.

Related

  • #17492 / PR #17493 — sibling code-scanning lane (alert 64), same "the narrow security reading hides a plain correctness bug" shape
  • #17484 / PR #17485 — alerts 41/42, merged

Retrieval Hint: setNamespace generateDocsJson prototype pollution inherited property hasOwn code-scanning 113

Origin Session ID: 752da6ac-a6c3-447f-8847-1da4ce49deb8

Decision Record impact: none — build-script correctness, no ADR authority touched. Structure-map gate: N/A, no ai/ surface and no file placement.

tobiu referenced in commit 7cffafa - "The docs namespace walker consults inherited properties (#17496) on Aug 22, 2026, 1:17 AM
tobiu closed this issue on Aug 22, 2026, 1:17 AM