LearnNewsExamplesServices
Frontmatter
id17534
titleNeo.merge accepts parsed prototype keys through its public API
stateClosed
labels
bugaitestingcoresecurity
assigneesneo-opus-grace
createdAtAug 22, 2026, 2:34 AM
updatedAtAug 22, 2026, 5:31 PM
githubUrlhttps://github.com/neomjs/neo/issues/17534
authorneo-gpt-emmy
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 22, 2026, 5:31 PM

Neo.merge accepts parsed prototype keys through its public API

Closed Backlog/active-chunk-18 bugaitestingcoresecurity
neo-gpt-emmy
neo-gpt-emmy commented on Aug 22, 2026, 2:34 AM

Context

Reviewing PR #17513 against its close target #17512 falsified the proposed dismissal premise. The exact Neo.merge implementation is part of Neo's default public export, and an independent runtime probe showed that a JSON-parsed __proto__ key reaches shared prototype state.

Live latest-open sweep: checked the latest 20 open issues and the 30 most recent all-state A2A messages at 2026-08-22T00:34:00.864Z; no equivalent successor or in-flight claim was found. The existing #17512 is the premise being superseded, not a duplicate implementation ticket.

The Problem

Neo.merge(target, source, defaults) recursively enumerates source with for…in, reads target[key], and writes back through that reference. For a source created by JSON.parse, __proto__ is an own enumerable key. Reading the matching property from a normal target reaches Object.prototype; the recursive write then mutates shared prototype state.

Exact falsifier against the implementation reviewed at PR #17513 (its src/Neo.mjs delta is comment-only):

const source = JSON.parse('{"__proto__":{"neo17513Probe":"reached"}}');
Neo.merge({}, source);
Object.hasOwn(Object.prototype, 'neo17513Probe'); // true

The probe was process-local and removed its sentinel after observation. This is a public-API reachability failure: enumerating only repository-internal callers cannot prove safety for application-defined consumers. The claimed 19-call census in #17512 also counted four static issue-archive strings as calls; excluding those archives yields 16 direct call expressions.

CodeQL alerts 62 and 63 therefore describe a real reachable behavior. Dismissing them as safe would encode a false security decision rather than resolve the defect.

The Architectural Reality

  • src/Neo.mjs owns the public Neo.merge primitive and exports the containing Neo object as the module default.
  • The hazardous traversal is centralized inside Neo.merge; fixing individual internal callers would leave the public contract unsafe.
  • src/worker/Base.mjs also feeds worker-message data / config objects into Neo.merge, reinforcing that the boundary must defend itself rather than rely on a repository call census.
  • GitHub's current code-scanning REST contract accepts false positive, won't fix, or used in tests; it does not accept the proposed used in a safe context. It also stores dismissed_comment, so repository documentation must not pretend the alert lacks durable reasoning.

The Fix

Harden Neo.merge at the public boundary so source keys cannot traverse or replace prototype-chain state. Preserve existing safe deep-merge and defaults behavior, and add a permanent RED-to-GREEN unit witness covering parsed JSON at the public API.

Do not add a dismissal ledger entry for alerts 62/63. Once the hardened implementation lands on dev, let CodeQL resolve the alerts as fixed.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.merge(target, source, defaults) src/Neo.mjs public method Safe keys retain current recursive/default merge semantics; prototype-chain traversal keys cannot mutate global or target prototype state No caller-side sanitizer; the primitive defends its own boundary Update JSDoc only if the chosen ignore/reject behavior changes caller-visible semantics Exact public-API parsed-JSON RED/GREEN unit proof
Global prototype integrity JavaScript object model + CodeQL alerts 62/63 A merge cannot add properties to Object.prototype or replace a normal target's prototype Fail closed for hazardous keys N/A Assert both prototype identity and sentinel absence after nested and defaults-path probes

Decision Record impact: none — localized security hardening of an existing core public utility; no accepted ADR is changed.

Acceptance Criteria

  • A permanent unit test proves the current parsed-JSON __proto__ payload is RED on pre-fix dev and GREEN at the PR head.
  • Neo.merge cannot mutate Object.prototype or replace a normal target's prototype through source or defaults, including nested payloads.
  • Existing safe deep-merge, array/value, and defaults semantics remain covered and green.
  • The implementation protects the public primitive itself; no caller census or caller-side sanitizer is used as the security boundary.
  • PR #17513's proposed source acceptance comment and dismissal rows for alerts 62/63 do not land.
  • [POST-MERGE] CodeQL alerts 62/63 transition to fixed on dev; if they remain open, create a new successor ticket rather than reopening this resolved one.

Out of Scope

  • A global CodeQL query exclusion or paths-ignore entry.
  • A general-purpose object sanitizer outside Neo.merge.
  • Unrelated mergeFrom / config-descriptor behavior.
  • Dismissing alerts 62/63.

Avoided Traps

Repository-call census as a public-API security proof. Internal callers cannot bound downstream application input.

Documenting acceptance before falsifying reachability. The exact runtime probe is the authority; prose and green CI cannot overrule it.

Fixing the alerts instead of the primitive. A dismissal or workflow exclusion would preserve the reachable mutation.

Related

  • Supersedes the prescription in #17512 and PR #17513.
  • Same CodeQL rule, different earlier defect: #17494 / PR #17496.
  • Live alerts: 62 and 63.

Origin Session ID: bbd4f722-ca03-4269-a88e-29555b12b9f9

Retrieval Hint: Neo.merge parsed JSON __proto__ CodeQL alerts 62 63 PR 17513 Drop Supersede

tobiu referenced in commit afd388c - "fix(core): Neo.merge refuses prototype-chain keys from parsed payloads (#17534) (#17537) on Aug 22, 2026, 5:31 PM
tobiu closed this issue on Aug 22, 2026, 5:31 PM