Skip to content

security(rail): local-only route sweep, public-creds structural guard, LTS security baseline and support window (rail 3.8.57) - #16209

Merged
diegosouzapw merged 3 commits into
release/v3.8.52from
security/rail-lts-sweep
Oct 10, 2026
Merged

diegosouzapw merged 3 commits into
release/v3.8.52from
security/rail-lts-sweep

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

Rail release 3.8.57, task 15 (security + compliance for the 3.9.0 LTS). It adds automated proof for Hard Rules #15/#17 (spawn-capable routes are loopback-only) and #11 (no public upstream credential literals), records the measured scanner, alert and provenance state, and adds the LTS support window to SECURITY.md. The window's duration is marked as a pending owner decision.

No production code changes. Two new tests, one new doc page, and SECURITY.md / docs/security/meta.json edits.

What changed

  • tests/unit/route-guard-local-only-sweep.test.ts (Hard Rules feat(npm): add npm package publishing with CLI entry point #15/fix(ci): explicit .npmrc auth for npm publish #17) walks the filesystem and asserts:
    • every route.ts under /api/mcp/, /api/cli-tools/runtime/ and /api/services/ is isLocalOnlyPath() with no method and for every write method. The same holds for the dashboard embed proxy /dashboard/providers/services/<name>/embed/**, which sits outside src/app/api and so outside check:route-guard-membership;
    • the only read-method exemptions inside those roots are the pinned /api/mcp/audit and /api/mcp/audit/stats, and both routes export only GET and do not spawn;
    • every in-scope route is classified MANAGEMENT, the route class whose policy evaluates isLocalOnlyPath();
    • the manage-scope carve-out is still /api/mcp/ only, and /api/services/ and /api/cli-tools/runtime/ stay in SPAWN_CAPABLE_PREFIXES;
    • the gate's SPAWN_CAPABLE_ROUTE_ROOTS still covers the three API roots;
    • any file under src/app/api that spawns directly belongs to a LOCAL_ONLY route. "Any file" includes helper modules next to route.ts. "Spawns" also covers fork(, execSync(, spawnSync( and a dynamic import("child_process").
  • tests/unit/public-creds-no-literals.test.ts (Hard Rule chore(ui): rebrand to OmniRoute #11) matches the credential values, while check:public-creds matches object keys. It looks for a Google OAuth client id, a GOCSPX- secret, an AIza key, a GitHub OAuth app id and a non-empty client_secret literal. It scans src/lib/oauth/, open-sse/ and src/shared/constants/ (the gate does not walk the last one). The only allowed form is a line that calls resolvePublicCred(). It also checks that publicCreds.ts stores its embedded defaults as masked byte arrays. A "gate blind spots" case runs the gate's own findLiteralCreds() on the same synthetic samples to show the gap. The fixtures are built at runtime, so the test file contains no string a secret scanner would match.
  • SECURITY.md: adds 3.9.x as the planned LTS line in Supported Versions and a new ## LTS support window (v3.9.x) section. It covers what stable/v3 receives, gives the duration as <T-GAP-3: owner decision pending — see ROADMAP.md>, and says LTS vulnerabilities go through the same private advisory channel.
  • docs/security/LTS_SECURITY_BASELINE.md (registered in docs/security/meta.json) records the state measured on 2026-10-10, with the command behind each number:
    • npm audit;
    • open code-scanning, Dependabot and secret-scanning alerts (0 / 0 / 0);
    • the five security workflows;
    • the OpenSSF Scorecard (6.5);
    • the npm publish provenance path: OIDC Trusted Publishing, and an SLSA v1 attestation on 3.8.49–3.8.51;
    • what the maintainer still has to do before the publish rehearsal.

Nothing was published, dismissed or closed.

Validation

Gate Command Result
New tests + related suites node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/route-guard-local-only-sweep.test.ts tests/unit/public-creds-no-literals.test.ts tests/unit/check-route-guard-membership.test.ts tests/unit/check-public-creds.test.ts tests/unit/security-route-guard-tiers.test.ts tests/unit/publicCreds.test.ts 71/71 pass
Mutation: drop the /api/services/ prefix same, sweep test 2 tests red
Mutation: add a GET exemption under /api/services/ same 1 test red
Mutation: helper module with fork() next to a route sweep test red; check:route-guard-membership stays green proves the added coverage
Mutation: bare client id in src/shared/constants/ + AIza key in an open-sse URL public-creds test red; check:public-creds stays green proves the added coverage
Lint npx eslint <new test files> 0 errors
Types npm run typecheck:core exit 0
Docs npm run check:docs-all pass
Docs tests security-docs-truthfulness-8097, docs-public-catalog-sensitive-pages, inference-auth-posture-13695 18/18 pass
Hard Rule #11 gate npm run check:public-creds OK
Hard Rules #15/#17 gate npm run check:route-guard-membership OK (105 routes, 0 new gaps)
Pack dry-run npm pack --dry-run --json --ignore-scripts 4,165 entries. check:pack-artifact was not run: no built dist/, and a full build is out of scope

⚠️ base-red inherited: #15306

Filesystem sweep proving Hard Rules #15/#17 for the LTS: every route.ts under
/api/mcp, /api/cli-tools/runtime, /api/services and the dashboard embed proxy
is LOCAL_ONLY for every write method, the only read exemptions inside those
roots are the pinned MCP audit routes, every one is classified MANAGEMENT (the
class whose policy runs the loopback gate), the manage-scope carve-out cannot
cover the spawning roots, and any src/app/api file (helpers included) that
spawns directly belongs to a LOCAL_ONLY route. Validated by mutation: dropping
the /api/services/ prefix, a new GET exemption, or a fork() helper module each
turn it red, while check:route-guard-membership stays green on the helper case.
Structural layer for Hard Rule #11 next to the key-based check:public-creds
gate: matches the credential formats themselves (Google OAuth client id,
GOCSPX- secret, AIza key, GitHub OAuth app id, non-empty client_secret
literal) anywhere in src/lib/oauth, open-sse and src/shared/constants, with
resolvePublicCred() lines as the only sanctioned form, and pins that
publicCreds.ts keeps its embedded defaults as masked byte arrays. Fixtures are
built at runtime so the test file itself carries no scanner-matching literal.
Validated by mutation: a bare const client id in src/shared/constants and an
AIza key in an open-sse URL turn it red while the gate stays green.
SECURITY.md gains the LTS support window section for v3.9.x (stable/v3
receives fixes, security patches and provider updates; reporting goes through
the same private advisory channel) with the window duration explicitly marked
as a pending owner decision (T-GAP-3), and lists 3.9.x as the planned LTS line
in Supported Versions.

docs/security/LTS_SECURITY_BASELINE.md records the measured 2026-10-10 state
with the command behind every number: npm audit, open code-scanning /
Dependabot / secret-scanning alerts, the five security workflows, OpenSSF
Scorecard, and the npm publish provenance path (OIDC trusted publishing +
SLSA provenance on 3.8.49-3.8.51) plus the gaps left for the rehearsal.
@diegosouzapw
diegosouzapw merged commit c73e274 into release/v3.8.52 Oct 10, 2026
12 of 13 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

@diegosouzapw
diegosouzapw deleted the security/rail-lts-sweep branch October 16, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant