Skip to the content.

maziyarpanahi/openmed - security scan

Repository: maziyarpanahi/openmed Commit scanned: f495e4e97ae8e92a3689941de8d0c7bbb825f704 Scan date: 2026-06-24 Disclosure status: public — clean scan, post-only (no upstream issue filed)

Summary

Severity Count
Critical 0
High 23
Medium 21
Low 0 (filtered at --min-severity medium)
Info 0

Total findings: 44 (0 real, reachable, undefended after curation)

OpenMed is a local-first healthcare AI library: clinical NER and HIPAA PII de-identification that runs 100% on-device, no cloud, no patient data leaving the network. It loads from a catalogue of 1,000+ HuggingFace medical models. That model-loading surface is exactly where a security review of this project should focus — and it is exactly where the maintainer got there first.

This is the 9th clean scan in the series. The 44 findings split into one genuinely scary class (arbitrary code execution while loading a model) that is already defended down to a named-CVE regression test, a 24-CVE dependency tail that is real but transitive and reachability-gated (and already wired to Dependabot), and the usual lint noise.

Top findings

1. transformers arbitrary code execution via malicious model — already defended

The defense is worth quoting because it is textbook. trust_remote_code defaults to False everywhere. The privacy-filter loader gates it behind an allowlist (is_trusted_for_remote_code) and the pipeline constructor refuses to pass trust_remote_code=True for any model outside the first-party openai/privacy-filter / OpenMed/privacy-filter-* prefix set:

if trust_remote_code and not is_trusted_for_remote_code(model_name):
    raise ValueError(
        f"Refusing to load {model_name!r} with trust_remote_code=True: "
        "model is not in the OpenMed trusted-remote-code allowlist."
    )

And there is a dedicated regression test — tests/unit/test_privacy_filter_security.py, headed with CVE-2026-47117 — that asserts the substring-bypass class is closed: attacker/foo-privacy-filter-bar, evil-org/my-privacy-filter, and username/privacy-filter are all rejected by the identifier matcher, while the legitimate openai/privacy-filter and case-variant OpenAI/Privacy-Filter pass. The scanner can flag the dependency CVE; it cannot see that the exact attack it implies was already modelled, fixed, and pinned with a test.

2. GitHub Actions shell-injection in convert-models.yml (6 sites) — privileged-trigger only

3. Dependency tail: 24 lockfile CVEs, real but reachability-gated

4. Dockerfile runs as root — scoped to the opt-in service container

Patterns observed

The review converges on the model-loading path, and the maintainer was already there. Almost every interesting security project in this series has one surface that is the threat model — for an MCP server it’s the credential keychain, for a multi-tenant harness it’s the tenant boundary. For a tool whose entire job is loading 1,000+ third-party models to touch PHI, it’s trust_remote_code. OpenMed doesn’t just default it off; it ships an allowlist, a constructor that hard-refuses out-of-allowlist remote code, and a regression test that names its own CVE and enumerates the substring-bypass attacker would try. That is the most security-literate handling of this exact class I’ve scanned.

This is a reachability scan, not a count scan. Trivy returns 24 lockfile CVEs; the honest number of reachable, undefended ones is zero. starlette needs the opt-in service extra; gitpython’s scariest CVE needs a calling pattern OpenMed doesn’t use; urllib3’s decompression bombs need an untrusted download peer where the peer is HuggingFace. This is the same lesson the Ouroboros and mistral-vibe scans taught — a version-match is a hypothesis, and the affected surface plus “is it reachable here?” is the test. The raw count overstates risk by the full count.

The security posture is baked into the dev loop, not bolted on. Pre-commit runs bandit (high-severity), gitleaks (custom .gitleaks.toml), and ruff; Dependabot is wired; there are CI release-gates. A solo academic maintainer (the project ships an arXiv paper and 1,000+ HF checkpoints) is running a tighter security pipeline than several commercially-backed repos in this series. The clean result isn’t an absence of attack surface — a PHI-touching model loader has plenty — it’s a maintainer who closed the surface first.

Notes on the tool

Disclosure timeline

This is a post-only clean-scan write-up. Curation found no real, reachable, undefended item that would warrant a courtesy issue — the one exploitability- shaped vector is already defended with a named-CVE regression test, and the dependency drift is on a configured Dependabot lane.

Reproduce

git clone https://github.com/maziyarpanahi/openmed /tmp/scan-target
python scanner/run_scan.py --repo /tmp/scan-target \
  --reports-dir ./reports/maziyarpanahi-openmed \
  --min-severity medium --ignore-samples