Skip to content

1.1.3 — security and correctness

Latest

Choose a tag to compare

@darthzen darthzen released this 09 Aug 04:10
14f17c7

Security and correctness release. Every finding below was reproduced with an executed PoC before it was fixed, and each fix has a test that fails without it.

Security

Sev
S2 High Path traversal. All 63 tool endpoint paths were f-strings interpolating a caller-supplied name, and httpx resolves .. against the base URL — so an argument selected the endpoint. A read-only server with only the compliance toolset reached GET /v1/user; nv_delete_group("../api_key/mcp-writer") issued DELETE /v1/api_key/mcp-writer, and the confirm handshake authorised it. Closed centrally in client.request.
S1 High Bearer scopes were decorative — parsed, handed to StaticTokenVerifier, never read. An nv:read token called nv_delete_group over real HTTP and the DELETE landed. Removed rather than enforced: privilege belongs to the NeuVector API key's role, narrowed by NV_READ_ONLY, NV_TOOLSETS and the handshake.
S9 High Credential leak. Any Settings validation failure rendered pydantic's input_value — the whole settings dict — so a typo'd NV_TOOLSETS printed NV_API_SECRET_KEY and NV_PASSWORD to the pod log.
S3 Med-High NV_ALLOWED_NAMESPACES was fail-open and overstated. nv_delete_group passed no namespace at all. Now wired up — 18 of 67 write paths are namespace-checked. The other 49 cannot derive a namespace, and that limit is now documented instead of implied away.
S5 Medium NetworkPolicy ingress had no from: selector, so every pod in the cluster could reach :8080 while the comment above it claimed "Nothing else, in or out."

Correctness and performance

Sev
P1 High Writes were retried. Every method was retried 3× on a transient failure, including ERR_FAIL_WRITE_CLUSTER and 502/504 — a failing POST /v1/group was sent three times. Retries are now GET-only, with a retry= opt-in.
P2 Medium Re-login stampede. Ten concurrent 401s produced ten POST /v1/auth, risking ERR_TOO_MANY_LOGIN_USER. Measured 10 logins before, 1 after.

⚠️ Before you roll this out

1.1.3 rejects a bearer token carrying a <token>:<scope> suffix at startup. That format was the 1.1.x convention and is now a hard configuration error, deliberately — silently stripping it would preserve belief in a control that never existed.

Fix the Secret first or the pod CrashLoops:

kubectl -n <ns> create secret generic neuvector-mcp-clients \
  --from-literal=bearer-tokens="$(kubectl -n <ns> get secret neuvector-mcp-clients \
     -o jsonpath='{.data.bearer-tokens}' | base64 -d | cut -d: -f1)" \
  --dry-run=client -o yaml | kubectl apply -f -

The token itself is unchanged, so clients need no reconfiguration — only the suffix comes off. Check for more than one comma-separated entry before using cut.

NV_ALLOWED_NAMESPACES also became honest rather than stronger. If you were relying on it to gate registry, webhook, sigstore or IAM writes, it never did — see the README section "What NV_ALLOWED_NAMESPACES does not cover."

Image

registry.ash4d.com/ash4d-lab/neuvector-mcp:1.1.3
sha256:484d127f507cc26051451b99e195ba6aa4c75493a79546bfb393f412a7c13489

linux/amd64, built on registry.suse.com/bci/python:3.13. The digest was verified by reading the tag back from the registry, not by trusting the builder's log.

Gate

119 tools, 700 passed / 1 xfailed, 97.65% coverage, 0 spec violations, ruff and mypy --strict clean.

Known divergence: SPEC.md was deliberately left frozen. SPEC 7.2 still describes the old all-methods retry policy and SPEC.md:260 still documents the removed scope format. Editing the spec to match the code would invert which one is authoritative.

Merged in #47 and #48.