Skip to the content.

ModelEngine-Group/nexent - security scan

Repository: ModelEngine-Group/nexent Commit scanned: ea3fd0711fb21310374b89737c7061f6777a1c58 Scan date: 2026-07-20 Disclosure status: post-only — strict-norm (private security policy); no real finding to disclose

Summary

Severity Count
Critical 0
High 33
Medium 82
Low 0
Info 0

Total findings: 115 (0 real, exploitability-shaped after curation) — 19th clean scan

What it is

Nexent is a zero-code platform (5.7k★, ModelEngine-Group) for auto-generating production-grade AI agents: a FastAPI backend that speaks its own JWT/CAS/Supabase auth, brokers MCP tool calls (fastmcp), runs a Ray/Celery data-processing tier, and ships a full Docker/Kong/Helm deploy stack. That is a lot of surface — a custom auth layer, a tool broker, a monitoring SQL tier, and a dozen CI/CD workflows — which is exactly why the 33 “highs” look alarming until you trace each one to where it’s actually called. On an agent platform the finding that matters is the auth boundary, and this one holds.

Top findings

There are no real, exploitability-shaped findings after curation. The interesting part is why each high-severity hit is a non-issue — grouped by the question it raised.

The auth boundary (the finding that would matter) — holds

Nexent implements its own JWT verification in backend/utils/auth_utils.py, and two Semgrep highs land here:

The monitoring SQL — parameterized-identifier FP

avoid-sqlalchemy-text (backend/apps/monitoring_app.py:92) builds a text(query_sql) with two f-string slots, which reads as injection on a per-tenant metrics query. But time_filter comes from _compute_time_range_filter, where hours = {"24h": 24, "7d": 168, "30d": 720}.get(time_range, 24) — the raw time_range string is never interpolated, only the resolved integer constant (always one of three, default 24). The tenant_filter uses a bound :tenant_id param. Every data value is bound; only whitelisted constants are formatted. This is the #1 recurring cluster in the series — the parameterized-SQL identifier false positive — in its smallest, cleanest form.

The CI/CD workflows — privileged-trigger hardening, not injection

11 run-shell-injection highs span build-offline-package.yml, docker-deploy.yml, docker-build-push-*.yml, and sdk_publish.yml. Every one of those workflows is triggered by workflow_dispatch — a maintainer manually clicking “Run workflow” with repo write access. An attacker can’t reach the interpolated inputs.version / inputs.deployment_mode steps without already being a maintainer. That’s the privileged-trigger side of the openmed lesson: workflow_dispatch = hardening, pull_request_target = vulnerability. Quoting the inputs is good practice; none of it is exploitable.

Infra & container hygiene — deployer’s call

The remaining highs are the deploy stack: ~7 detect-insecure-websocket (ws:// in Kong / docker-compose / Helm — internal service-mesh config), and ~8 Dockerfile last-user-is-root / missing-user + Trivy DS-0002/DS-0025 (containers running as root). Standard container-hardening notes for a self-hosted stack, not app-level vulnerabilities. The lone Gitleaks hit (deploy/docker/deploy.sh:622) is elastic:${ELASTIC_PASSWORD:-nexent@2025} — a default-password fallback in the bootstrap script, overridable by env. Weak default worth documenting; not a leaked live credential.

The 82 mediums are almost entirely github-actions-mutable-action-tag (pin third-party actions to a commit SHA rather than a tag) — the same CI-lint bulk that inflates every well-tooled repo’s count.

Patterns observed

Count scales with surface, not risk — again. 115 findings, 0 real. Nexent has one of the widest surfaces in this series (custom auth + MCP broker + SQL monitoring + Ray tier + full Docker/Kong/Helm deploy), and the raw number tracks the number of moving parts, not the number of ways in. Almost every high is an infrastructure-config or CI-lint hit that a scanner can’t contextualize: it sees ws:// and a root Dockerfile user and a shell step interpolating an input, but not that the WebSocket is internal, the container is a deploy artifact, and the workflow only a maintainer can trigger.

The auth layer is where a custom platform earns or loses trust, and nexent built it correctly. Rolling your own JWT verification is where most agent platforms slip — unpinned algorithms, verify_signature=False on the wrong path, a fallback secret that the verifier also accepts. Nexent avoids all three: signature always enforced, HS256 pinned, secret env-loaded and fails-closed, and the one unverified decode is scoped to logout-idempotency with a server-side CAS session check behind it. The hardcoded secret is genuinely test-only. This is the mark of a team that understood the threat model of its own auth code.

Dependencies pinned by floor, audited by nobody’s default tooling. The backend pyproject.toml pins with >= floors (fastapi>=0.115.12, authlib>=1.3.0, PyJWT>=2.8.0, cryptography>=42) plus defusedxml for XXE defense — so a fresh resolve pulls current patched versions. One nuance worth a deployer’s eye: fastmcp>=2.14.2,<3.0 caps below the 3.x line, so any fix that only landed in fastmcp 3.x wouldn’t be reachable without lifting the ceiling. See the tool note below on why this run didn’t audit those deps directly.

Notes on the tool

Disclosure timeline

Reproduce

git clone https://github.com/ModelEngine-Group/nexent /tmp/scan-target
python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/modelengine-group-nexent --min-severity medium

Generated by AI PatchLab. Findings are curated: noise filtered, top items highlighted, threat paths traced by hand. This is a point-in-time signal, not an audit.