Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Validate federation consumers before replacing their UUID dependency

On 2026-10-27, work for #186 started from dev
`ef495eff15ed7aac51e72dc7a1db1bb96d2b3695` with Node 22.18.0/npm 10.8.2.
`npm explain uuid` identified Core's direct UUID 9.0.1 and
`@neo4j/graphql@5.12.15 → @apollo/subgraph@2.14.4 →
@apollo/federation-internals@2.14.4 → uuid@9.0.1`.
Server UUID 11.1.1 and Mermaid UUID 14.0.3 were separate, already patched paths.

A bounded direct call on the baseline gave the same result for UUID v3 and v5:
a one-byte output buffer was modified and no error was thrown. The advisory
[GHSA-w5hq-g745-h8pq](https://github.com/advisories/GHSA-w5hq-g745-h8pq)
identifies 11.1.1 as patched. Inspected application calls use unbuffered v4;
federation's `src/operations.ts` uses unbuffered v1 for deferred-directive keys.
This source observation does not establish safety for future callers.

## Cause and correction

Updating subgraph to 2.15.1 removed the old federation dependency, but a real
`Neo4jGraphQL.getSubgraphSchema()` probe with a federation v2.3 `@link` failed:
`TypeError: doc.definitions is not iterable` in `concatAST`. The installed
subgraph's `modulesFromSDL` no longer normalizes Neo4j's single
`{ typeDefs, resolvers }` input. The initial probe also omitted `@link`; that
fixture error was corrected before observing this compatibility failure.

Retain subgraph/federation 2.14.4. Upgrade Core directly to UUID ^11.1.1 and
scope the transitive override to `@apollo/federation-internals@2.14.4` only.
UUID 11 retains CommonJS support; UUID 12 removes it. The existing server UUID
copy deduplicates to 11.1.1; Mermaid remains 14.0.3. No other package version
changes are part of this fix.

Adding an override and running `npm install --package-lock-only` initially left
the old root UUID resolution. The lock and buffer regression failed, exposing
that stale result. `npm update uuid --package-lock-only --legacy-peer-deps`
then `npm ci --legacy-peer-deps --no-audit --no-fund` installed 1,220 packages;
`npm ls uuid @apollo/subgraph --all` confirmed the intended consumer paths.
Do not infer the installed result from an override declaration alone.

## Evidence and limits

`node --test tests/lib/dependencies/uuid.test.mjs`: **4 passed**, covering every
locked UUID, actual consumer-relative CommonJS and ESM imports, valid generated
IDs, invalid v3/v5/v6 buffer bounds without partial writes, and real Neo4j
federation schema generation. An attempted `_service` execution without a driver
was a fixture error; schema generation/printing needs no database. Database-backed
execution remains the separate real-Neo4j contract gate.

A fresh-cache `npm audit --omit=dev --json` after the correction reported
**2 moderate router entries and 1 high Undici entry**, with no UUID/federation
finding. Undici is separately addressed by #194; that audit preceded composition with
merged dev #194. This count is dated dependency metadata, not a live exposure or
full-security claim. Complete runtime, browser and final CI results belong in
the associated PR discussion; no merge/deployment is established here.

Corrective rule: upgrade the narrowest compatible consumer chain, keep the
broken-upstream evidence, test both module systems and the actual schema API,
and verify the resolved lock and installed paths after a clean install. Revisit
the version-scoped override when federation changes; do not widen it silently.

## Review and composed validation

Independent review found that a simple `>=11.1.1` lock guard would wrongly
accept affected 12.0.0 and 13.0.0. The regression now uses the complete advisory
union and affected/fixed controls on every listed major, including malformed
versions and an unfixed prerelease. The selected 11.1.1/14.0.3 versions were
already patched; the correction prevents a future false green.

Composition `aba24a34d193b60e2318cd5b197f66e64f320215` incorporates reviewed dev
`32149ee61ea5436831d95d76e4c4b6dc2db619a3`. A clean install and fresh installed
production audit reported **2 moderate router entries and 0 high/critical**.
All four package unit suites passed with one task/worker at a time, retaining
their original timeouts, and the composed harness passed 167/167 before the
additional advisory-range control. An earlier concurrent web run hit the
existing five-second save-test deadline under host load; its single-worker
rerun passed 326/326. The following earlier harness ran while composition was
changed and is discarded as provenance, not counted as validation. Freeze the
source/dependencies throughout a run and use bounded local concurrency. The
final advisory-test change requires its own harness run and exact-head CI;
results are recorded in the PR discussion. No failed run was erased.
1 change: 1 addition & 0 deletions .agents/issues/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
| `mergePullRequest` refuses a workflow update without OAuth `workflow` scope | [Recorded API refusal and same-account SSH identity check](../inbox/2026-10-23-pr-base-ref-is-not-live-tip.md#a-workflow-scoped-api-refusal-needs-its-own-evidence); authentication alone does not prove write authorization or merge completion |
| MCP tool calls succeed but the stdio client reports JSON parse errors from startup, CPU or consistency messages | [Successful MCP calls do not prove a clean stdio protocol](../inbox/2026-10-23-mcp-stdio-diagnostics.md); `packages/mcp-server/tests/neo4j-contract.test.ts` checks real subprocess traffic and stderr diagnostics |
| `dev` has an owner-authorized admin exemption; a review-rule blocker or docs-only change tempts skipping validation | [Dev admin integration playbook](../playbooks/dev-admin-integration.md); [dated authorization and merge evidence](../inbox/2026-10-23-dev-admin-integration.md). Admin authority is not a non-author approval; verify final branch CI before closeout |
| A federation upgrade removes UUID findings but breaks Neo4j schema generation; an override leaves the old lock resolution | [Validate federation consumers and installed UUID paths](../inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md); `tests/lib/dependencies/uuid.test.mjs` |
1 change: 1 addition & 0 deletions .agents/issues/security-audit-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
| Nested lint glob stalls the process | `tests/lib/dependencies/minimatch.test.mjs`; scoped parser matcher override and bounded child-process regression |
| A fresh audit reports Zod high severity after an earlier green scan; bounded collections still walk rejected elements | [Zod consumer and bounded-validation lesson](../inbox/2026-10-23-zod-constrained-collections.md); recheck the resolved SDK dependency and use an isolated audit cache without inferring why older metadata differed |
| Browser login lockout is mistaken for server throttling; one-use email token succeeds concurrently | [Remaining authentication work](../../docs/quality/SECURITY_SCOPE_2026-10-23.md#remaining-authentication-work) |
| A federation upgrade removes UUID findings but breaks Neo4j schema generation; an override leaves the old lock resolution | [Validate federation consumers and installed UUID paths](../inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md); `tests/lib/dependencies/uuid.test.mjs` |
15 changes: 15 additions & 0 deletions docs/quality/SECURITY_SCOPE_2026-10-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ Source locations are relative to `packages/server/src/` and anchored to the snap
## Coverage still needed

The folder tests do not establish all generated Graph/WorkItem/Edge permissions, concurrent deletion integrity, MCP/agent grant revocation, share-link scopes, session/cookie policy, uploads, backups, email delivery, or deployed proxy controls. Those need isolated contracts and user-level tests with current roles, multiple accounts, races, and negative cases. Dependency counts, passing smoke tests, and absence of a discovered source sink are separate pieces of evidence; none is a full security assessment.

## UUID/federation follow-up on 2026-10-27

The #186 UUID correction starts from dev `ef495eff15ed7aac51e72dc7a1db1bb96d2b3695`.
Core's direct UUID 9.0.1 becomes ^11.1.1; federation-internals 2.14.4 has a
version-scoped UUID 11.1.1 override. Server deduplicates to that patched copy;
Mermaid retains 14.0.3. Subgraph/federation remain 2.14.4 after a 2.15.1 schema
compatibility failure. The [consumer lesson](../../.agents/inbox/2026-10-27-federation-consumers-bound-uuid-upgrades.md)
records negative controls, exact paths, clean install and four dependency
regressions. The pre-composition fresh-cache production audit had two moderate router
entries and high Undici. After composing merged dev #194 at `aba24a34d193b60e2318cd5b197f66e64f320215`,
a clean install and a second fresh-cache installed audit reported **two moderate
router entries, zero high/critical entries**, and no UUID/federation finding.
The earlier inventory above remains historical. No Tailwind declaration was reclassified.
This change does not close the router, CSS, dev-tool or hosted acceptance work.
21 changes: 5 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@
"@graphql-tools/utils": "12.1.0",
"@typescript-eslint/typescript-estree@6.21.0": {
"minimatch": "9.0.7"
},
"@apollo/federation-internals@2.14.4": {
"uuid": "11.1.1"
}
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"clean": "rm -rf dist coverage"
},
"dependencies": {
"uuid": "^9.0.1",
"uuid": "^11.1.1",
"neo4j-driver": "^5.15.0"
},
"devDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions tests/lib/dependencies/uuid.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import test from 'node:test';

const root = new URL('../../../', import.meta.url);
const require = createRequire(new URL('package.json', root));
const coreRequire = createRequire(new URL('packages/core/package.json', root));
const federationRequire = createRequire(require.resolve('@apollo/federation-internals'));
const semver = require('semver');
const affectedRange = '<11.1.1 || >=12.0.0 <12.0.1 || >=13.0.0 <13.0.1';
const hasBufferFix = version => Boolean(semver.valid(version))
&& !semver.satisfies(version, affectedRange, { includePrerelease: true });

test('the advisory guard rejects affected releases on every maintained major', () => {
for (const version of ['9.0.1', '11.1.0', '11.1.1-beta.0', '12.0.0', '13.0.0', 'invalid']) {
assert.equal(hasBufferFix(version), false, version);
}
for (const version of ['11.1.1', '12.0.1', '13.0.1', '14.0.3']) {
assert.equal(hasBufferFix(version), true, version);
}
});

test('every locked UUID path has the buffered-output fix', async () => {
const lock = JSON.parse(await readFile(new URL('package-lock.json', root), 'utf8'));
const paths = Object.entries(lock.packages).filter(([path]) => /(^|\/)node_modules\/uuid$/.test(path));
assert.ok(paths.length > 0);
for (const [path, entry] of paths) {
assert.ok(hasBufferFix(entry.version), `${path}: ${entry.version}`);
}
});

test('Core CommonJS and ESM consumers generate valid unbuffered UUIDs', () => {
const cjs = coreRequire('uuid');
const ids = Array.from({ length: 100 }, () => cjs.v4());
assert.equal(new Set(ids).size, ids.length);
for (const id of ids) {
assert.ok(cjs.validate(id));
assert.equal(cjs.version(id), 4);
}
const esm = spawnSync(process.execPath, ['--input-type=module', '-e', `
import assert from 'node:assert/strict';
import { v4, validate, version } from 'uuid';
const id = v4();
assert.ok(validate(id));
assert.equal(version(id), 4);
`], { cwd: new URL('packages/core/', root), encoding: 'utf8', timeout: 5000 });
assert.equal(esm.error, undefined);
assert.equal(esm.status, 0, esm.stderr);
const federation = federationRequire('uuid');
assert.equal(federation.version(federation.v1()), 1);
});

test('Core and federation UUID reject invalid buffers without partially writing', () => {
for (const consumer of [coreRequire, federationRequire]) {
const uuid = consumer('uuid');
for (const name of ['v3', 'v5', 'v6']) {
for (const [length, offset] of [[1, 0], [16, 1], [16, -1]]) {
const buffer = new Uint8Array(length).fill(42);
const original = buffer.slice();
const args = name === 'v6' ? [{}, buffer, offset] : ['graphdone', uuid[name].DNS, buffer, offset];
assert.throws(() => uuid[name](...args), RangeError, `${name}: length=${length} offset=${offset}`);
assert.deepEqual(buffer, original);
}
}
}
});

test('Neo4j builds its federation schema through the installed Apollo consumer', async () => {
const { Neo4jGraphQL } = require('@neo4j/graphql');
const { printSubgraphSchema } = require('@apollo/subgraph');
const neo = new Neo4jGraphQL({ typeDefs: `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type WorkItem @key(fields: "id") { id: ID! @id title: String! }
` });
const schema = await neo.getSubgraphSchema();
const sdl = printSubgraphSchema(schema);
assert.match(sdl, /type WorkItem\b/);
assert.match(sdl, /@key\(fields: "id"\)/);
assert.ok(schema.getQueryType().getFields()._service);
assert.ok(schema.getQueryType().getFields()._entities);
});
Loading