Skip to the content.

Zleap-AI/SAG — security scan

Repository: Zleap-AI/SAG Commit scanned: f1b4879c41974e82bb958335a2d29c767530ac26 Scan date: 2026-08-27 Disclosure status: public courtesy issue filed (no private channel advertised — no SECURITY.md at root, .github/, or docs/; private vulnerability reporting disabled)

Summary

Severity Count
Critical 0
High 44
Medium 13
Low 0
Info 3

Total findings: 60 (1 real after curation, plus one dependency-freshness note)

SAG is an original-architecture RAG knowledge base — a FastAPI backend (apps/api), a Next.js web client (apps/web), and an Electron desktop wrapper (apps/desktop) — shipped as a single-user personal app with a default docker compose bind of 127.0.0.1.

The scanner’s 44 “high” findings collapsed to one real item under curation. The one that matters is not in the scanner’s list in any recognizable form: it is a missing authentication check, and a rule that greps for dangerous calls cannot see a check that never happens.

Top findings

1. Login never verifies the password — any name logs in as the owner

2. Web dependency freshness (secondary — hardening, not a live bug)

Patterns observed

This is well-built code, and the scanner’s raw output actively hides that. Every one of the six SQLAlchemy text() / raw-query “high” findings is an identifier-only interpolation — a table name drawn from a hard-coded map or from sqlite_master, with the actual values bound through bindparam(...) or ? placeholders right beside the flagged line. The three child_process / spawn(shell=True) findings are all dev, build, and release scripts spawning fixed argv (npm run build), not runtime attack surface. The fourteen “generic-api-key” secrets are SHA-256 tree digests in .public-sync-state.json and fixtures in test_*.py. Curation turned 44 highs into zero.

What the app does well is the more interesting half. The upload path (attachments.py) generates its own UUID filenames, validates retrieval with a strict ^[0-9a-f]{32}\.(png|jpe?g|webp|gif)$ regex, caps size, and requires auth on both ends. The MinerU result fetcher resolves the host and rejects any address that is_global fails — a real SSRF guard. The Dify compatibility endpoint — the one route that is deliberately unauthenticated — gates on a constant-time compare_digest API key and fails closed when the key is unset. The prod secret-key guard is enforced at startup. The two archive-handling paths read entries in memory (archive.read(...)) and never extract to disk, so there is no zip-slip.

So the single real finding sits inside a codebase that clearly understands security — which is exactly the shape these scans keep surfacing. The bug is not a missing seatbelt on obviously dangerous code; it is one control (the login) not enforcing a boundary that three other controls (registration password, prod key guard, registration lock) all assume exists. No static rule sees a boundary that isn’t there. You find it by tabulating every route against its guard, noticing the one login path that resolves identity without a credential, and then running it.

Notes on the tool

Disclosure timeline

Reproduce

git clone https://github.com/Zleap-AI/SAG /tmp/scan-target
python scanner/run_scan.py --repo /tmp/scan-target --reports-dir ./reports/zleap-ai-sag --min-severity medium
# dependency surface the root-only scan missed:
cd /tmp/scan-target/apps/api && pip-audit .