Skip to the content.

msoedov/agentic_security — security scan

Repository: msoedov/agentic_security — 1.9k★, Apache-2.0, an Agentic LLM vulnerability scanner / AI red-teaming kit (i.e. a dynamic-behavior security tool, complementary to AI PatchLab’s static-code focus). Commit scanned: 2aabcef (HEAD of main at scan time) Scan date: 2026-05-15 Disclosure status: Two findings filed as a public courtesy issue on the agentic_security repo. One additional finding was reported privately via GitHub’s Private Vulnerability Reporting (active on this repo) and will be added to this write-up after the maintainer’s response.

Summary

Severity Count
Critical 0
High 6
Medium 3
Low 0
Info 0 (filtered)

9 total findings. After curation: 2 best-practice issues worth flagging publicly, 1 finding disclosed privately, and 6 false positives or out-of-scope items.

This is the first scan in our series with multiple findings substantive enough to warrant action — and the first one where a finding hit the bar for private disclosure ahead of public publication. Scanning a security tool tends to be illuminating in both directions.

Top findings (curated, public)

1. agentic_security/middleware/cors.py:10 — wildcard CORS with credentials

Tool: Semgrep (wildcard-cors, medium confidence) Verdict: Real misconfiguration.

origins = ["*"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

The combination of allow_origins=["*"] with allow_credentials=True is explicitly forbidden by the CORS specification. Most browsers will refuse to honor it (they’ll silently strip credentials from cross-origin requests), but the server’s intent is to accept any origin with credentials — which would, if the spec didn’t intervene, let any malicious page running anywhere read authenticated responses from the agentic_security server.

The fix is to enumerate explicit allowed origins (or accept that credentials should be off for an * origin policy):

origins = ["http://localhost:3000", "https://your-frontend.example"]
# or, if credentials genuinely aren't needed cross-origin:
# allow_credentials=False

2. agentic_security/routes/static.py:104 — path-traversal risk in the icons proxy

Tool: Semgrep (ssrf-requests, medium confidence) Verdict: Plausibly exploitable — defense-in-depth recommended.

@router.get("/icons/{icon_name}")
async def serve_icon(icon_name: str) -> FileResponse:
    icon_path = ICONS_DIR / icon_name
    if not icon_path.exists():
        url = f"https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark/{icon_name}"
        response = requests.get(url)
        if response.status_code == 200:
            icon_path.write_bytes(response.content)
        ...
    return get_static_file(icon_path, content_type="image/png")

Two related concerns:

  1. icon_name is interpolated into a filesystem path (ICONS_DIR / icon_name) without validation. FastAPI’s {icon_name} path parameter is single-segment by default, but URL-encoded path separators (%2F) can still slip through in some Starlette versions, and the parameter is also used to write a file via icon_path.write_bytes. A malicious or malformed value could lead to writing outside ICONS_DIR.

  2. Same value is interpolated into the upstream URL to registry.npmmirror.com. The host is fixed, so this isn’t a classic SSRF (no choosing the target). But an attacker can make the server cache an arbitrary file from registry.npmmirror.com under a name they control.

Tactical fix: validate icon_name against a strict allowlist before either filesystem access or the upstream request:

ICON_NAME_RE = re.compile(r"^[a-z0-9_-]+\.png$")
# in the handler:
if not ICON_NAME_RE.match(icon_name):
    raise HTTPException(status_code=400, detail="Invalid icon name")

Defense in depth: also resolve and verify the constructed path stays inside ICONS_DIR with icon_path.resolve().is_relative_to(ICONS_DIR.resolve()).

3-9. False positives and out-of-scope items (still worth noting for tooling feedback)

Finding File Verdict
direct-use-of-jinja2 agentic_security/routes/static.py:18 FP — autoescape=True is explicitly set in the same Environment(...) call. Semgrep’s rule didn’t read the kwargs.
Potential secret detected: stripe-access-token tests/unit/refusal_classifier/test_pii_detector.py:14 By design — test data for a PII detector. The string sk_test_1234567890abcdef is literally the fixture that exercises the detector’s API-token rule.
Potential secret detected: generic-api-key tests/unit/test_security.py:128 By design — test data for a log-sanitization function. The fixture is 'api_key="sk-1234567890"' and the test asserts that the sanitizer removes it.
prototype-pollution-loop agentic_security/static/vue.js:518 Out of scope — vendored Vue.js, not the project’s code.
python37-compatibility-importlib2 agentic_security/refusal_classifier/model.py:1 Not security — a Python 3.7 compatibility hint, irrelevant given the project requires >= 3.12.

Patterns observed

Scanning a security tool sharpens the contrast between syntactic patterns and intent. Three of the seven non-private findings here (Stripe token in test_pii_detector, API key in test_security, and the jinja2.Environment with autoescape=True) are textbook examples of static scanners firing on the shape of a pattern without understanding the role of the surrounding code. A PII detector that doesn’t test against fake-but-realistic credentials would be a useless PII detector. A jinja2 Environment that explicitly sets autoescape=True is the opposite of the vulnerability that the rule is designed to catch. The signal in static-scanner output lives in the rule names; the noise lives in the next half-step of context.

Two findings clear that bar: the wildcard-CORS misconfiguration is unambiguous, and the icons-proxy route has both filesystem-write and upstream-fetch paths that warrant defense-in-depth even if today’s FastAPI behavior makes the worst-case exploit narrow. These are textbook web-app hygiene items that any maintainer would want surfaced.

One finding warranted private disclosure instead of immediate publication. Agentic Security has Private Vulnerability Reporting enabled on the repo, which is exactly the channel a third-party scanner should use when a finding’s exploitability depends on context the reporter can’t determine from the outside. This page will be updated with the resolution after the maintainer responds — a delayed-disclosure section, not a swept-under-the-rug omission.

The meta-angle: this is the first scan in our series where AI PatchLab found more than just curation-fodder false positives. The fact that the target is itself a security tool isn’t ironic — it’s confirming that static code analysis and dynamic agentic red-teaming are non-overlapping specialties, and that the maintainers of one specialty are exposed to the blind spots of the other.

Notes on the tool

Recurring items from prior scans (still in the backlog):

New items from this scan:

Disclosure timeline

Reproduce

git clone https://github.com/elfrost/ai-patchlab
cd ai-patchlab
pip install -e ".[dev]"
python scanner/run_scan.py \
  --from-git-url "https://github.com/msoedov/agentic_security" \
  --reports-dir reports/msoedov-agentic-security \
  --min-severity medium

External tools (Semgrep, Gitleaks, Trivy, pip-audit) need to be installed separately — see the project README.