One Fix, One Docstring, One Untouched: Three agno CVEs Record What a 42,000-Star Agent Framework Decided to Treat as a Bug
Three NVD records published within half a second of each other this morning — CVE-2026-108816, CVE-2026-108817 and CVE-2026-108818, all at 09:16:43Z on 12 October 2026, all assigned by cna@vuldb.com — name agno, the Python agent framework with 42,683 stars and 6,120 forks whose repository was pushed to again an hour before the records appeared. All three originate in public GitHub issues filed on 8 September 2026 by the researcher ybyu-ieu, each with a working, dependency-free proof of concept and each self-scored 9.8 critical.
The interesting part is not the three findings. It is what happened to them in the 34 days between the reports and the records. On 16 September, inside ninety minutes, agno’s maintainers merged three pull requests touching exactly these files. One of them is a serious, well-tested code fix. One of them changes no behaviour at all and rewrites a docstring to say the control the researcher attacked was never a control. The third report, against the server runtime, drew no commit at all. We read every file at main HEAD this morning to establish which is which.
The one that got fixed: CodingTools run_shell (CVE-2026-108817)
5.5 medium on CVSS 3.1 (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L), 2.0 low on CVSS 4.0, CWE-77/CWE-78, affecting agno through 3.0.9 and fixed in 3.0.10. This is the only one of the three with a fixed version, and the record names the patch commit directly.
The reported defect: CodingTools.run_shell() handed its whole command string to subprocess.run(command, shell=True) after screening it against a nine-entry substring disallowlist of shell operators (&&, ||, ;, |, $(, backtick, >, >>, <). The default mode is restrict_to_base_dir=True, python is the first entry of the default allowlist, and tokens beginning with - were skipped outright — so python -c "exec(chr(111)+chr(112)+…)" contained no disallowed substring, passed the allowlist on its first token, and executed arbitrary Python on the host. The report’s central argument is a mechanism argument, not a list-completeness one: a substring filter that reconstructed characters defeat cannot be fixed by lengthening the list.
The maintainer response accepted exactly that framing. PR #10210 (merged 15:01Z, 16 September) stops pattern-matching payload content and rejects the flags instead, with an options parser that follows CPython’s own rules — attached arguments (-c'code'), clustered short options (-Ic), -m, a bare - for stdin, and value-taking options (-W, -X, -Q) whose argument happens to be c:
_CODE_EXEC_INTERPRETER_PREFIXES: tuple = ("python",)
_CODE_EXEC_SHORT_OPTS: set = {"c", "m"}
_ARG_TAKING_SHORT_OPTS: set = {"W", "X", "Q"}
The same PR flips enable_run_shell from True to False — the shell tool is now opt-in — and adds ten assertions covering the reported bypass, including the negative controls (python3 ok.py and python3 -W c ok.py must still run). PR #10220, merged ninety minutes later, removes shell=True from restricted mode entirely: commands are tokenised and executed with shell=False, which makes chaining, redirection, substitution and globbing inert rather than filtered. The maintainer closed the issue by re-running the researcher’s own chr() payload against the merged code and reporting that the marker file was not created. Version 3.0.10 shipped at 17:41Z the same day. Judged on its own, this is how it is supposed to work: report with a PoC, fix the mechanism rather than the symptom, test the bypass, ship within hours, confirm publicly.
The one that got a docstring: PythonTools run_python_code (CVE-2026-108816)
6.3 medium on CVSS 3.1 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L), 2.1 low on CVSS 4.0, CWE-74/CWE-94, affecting through 3.0.6, no fixed version. The NVD description ends: “The project was informed of the problem early through an issue report but has not responded yet.”
That sentence is not quite right, and the way in which it is wrong is the story. agno did respond, on the same afternoon as the run_shell fix. PR #10218 — titled, in the maintainers’ own words, fix: document PythonTools has no code sandbox — was created at 14:58Z and merged at 15:00:41Z on 16 September, sixty-five seconds before the CodingTools fix landed.
The report concerned two lines. PythonTools executes agent-supplied code with exec(code, self.safe_globals, self.safe_locals), and the namespaces those parameters name are initialised like this:
self.safe_globals: dict = safe_globals or globals()
self.safe_locals: dict = safe_locals or locals()
In the September release those lines sat under a comment reading “Restricted global and local scope.” The researcher’s point was sharper than “the default is permissive”: because the initialisation is a falsy-or rather than an is None check, a caller who passes safe_globals={} precisely in order to restrict execution still receives the module’s real globals(), complete with __builtins__ and therefore __import__. A restriction API that cannot be engaged even by a caller who tries is not a weak boundary; it is an absent one with a reassuring name.
We read libs/agno/agno/tools/python.py at main HEAD this morning and at the current release tag v3.1.2. Both lines are present, unchanged, at line 71 and 72. What changed is everything around them. The warning emitted on first use now reads “PythonTools executes arbitrary Python in this process … safe_globals/safe_locals and restrict_to_base_dir are not a sandbox.” The class docstring now carries a .. warning:: block stating that there is no sandbox, that this is “an RCE sink if the agent is prompt-injected,” and that safe_globals/safe_locals “are NOT a security boundary despite the name.” The parameter documentation now says the same thing twice more. The seven tools, including run_python_code and save_to_file_and_run, are still registered unconditionally in the constructor — there is no enable_ flag on the execution tools, unlike the run_shell treatment next door.
Our editorial reading: two reports made the identical argument — this named safety mechanism cannot be engaged — against two files in the same library, on the same day, from the same researcher. One was accepted as a defect in the mechanism and received a parser, a default change and a regression suite. The other was accepted as a defect in the documentation of the mechanism and received better prose. We do not think the prose is cynical; it is unusually candid, and candour about a sink is worth more than most vendor hardening notes. But documenting that safe_globals is not safe, while leaving it named safe_globals and leaving the falsy-or that silently discards a caller’s explicit {}, keeps the trap and relabels the floor around it. The one-line change the report asked for first — if safe_globals is None — costs nothing and is still not there.
The one that got nothing: AgentOS authentication (CVE-2026-108818)
7.3 high on CVSS 3.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L), 5.5 medium on CVSS 4.0, CWE-287/CWE-306, affecting through 2.7.3/3.0.6, no fixed version. Issue #10051 is still open with zero comments since it was filed on 8 September.
AgentOS is agno’s “serve your agents over HTTP” runtime. The report describes a two-layer fail-open. The constructor defaults authorization to False and the settings default os_security_key to None, so the gate that decides whether to install the authentication middleware — bool(self.authorization or jwt_env_configured or security_key) — is false out of the box and no middleware is attached. The per-route dependency that every REST router depends on is then the second layer, and it permits rather than rejects:
if not settings or not settings.os_security_key:
return True
We read that branch at tag v3.0.6 (line 240) and at main HEAD this morning (line 630). It is identical. In between, auth.py grew from 614 lines to 1,122 — service-account tokens, caching, a _constant_time_equal helper that replaced the plain token == settings.os_security_key comparison of v3.0.6. That last change is a genuine improvement, and it is worth noticing what it protects: a timing side channel available to an attacker who is already guessing a security key that an operator has configured. The branch that admits every caller when no key is configured — the state the report is about — was not touched while the function around it was rewritten.
Read in the framework’s own terms, this is the familiar insecure-default argument, and the familiar counter-argument (“serve() binds localhost; operators opt in”) is available. The report answers it with agno’s own cookbook example binding 0.0.0.0, and we would add the composition that makes this batch coherent: AgentOS with no authentication, serving an agent that carries PythonTools, whose execution tools are on by default and whose namespace restriction cannot be engaged, is an unauthenticated HTTP endpoint in front of exec(). Each component is defensible in isolation and documented as the operator’s responsibility. The product of the three is not a configuration choice anyone made.
The scoring is incoherent, and the same catalogue proves it
The researcher scored all three at 9.8. VulDB, as assigning CNA, scored them 6.3, 5.5 and 7.3 on CVSS 3.1 and 2.1, 2.0 and 5.5 on CVSS 4.0. All three NVD records carry vulnStatus: Deferred at publication, with the VulDB CVSS 3.1 metric recorded as Primary — there is no independent NVD analysis coming to adjudicate.
The CVSS 4.0 vectors show where the gap comes from. For the two code-execution findings, VulDB records VC:L/VI:L/VA:L — low confidentiality, integrity and availability impact for arbitrary code running in the host process with the framework’s privileges — plus PR:L, treating the agent as a privileged caller. The same catalogue then scores the no-authentication finding PR:N and lands at 5.5, so within one batch the server with no password outranks the shell it fronts.
For calibration, the same product already has a record for the same sinks. CVE-2026-37003, published 27 August 2026 with cve@mitre.org as the assigning source, states that agno through 2.5.8 is vulnerable to RCE via prompt injection because “PythonTools and ShellTools … pass unsanitized, LLM-generated arguments directly to execution sinks including exec(), runpy.run_path(), and subprocess.run(),” and it carries a 9.8 secondary CVSS 3.1 score. Five weeks later the same exec() call in the same file is 6.3, and 2.1 on the newer scale. Nothing about the code changed between those two records. This is the practical consequence for anyone running a severity-threshold policy over agent frameworks: the number attached to “the tool that runs code, runs code” depends entirely on who filled in the form, and a 7.0 cutoff would have surfaced the server misconfiguration while filtering out both remote-code-execution paths.
Four more advisories, and one identifier still in flight
The researcher’s public advisory repository, created 6 September and last pushed 8 September, contains seven advisories. Three became CVEs this morning. The remaining four, read first-hand as filenames and headlines, are: agent orchestration executing model-requested tool calls with no default confirmation gate (CWE-913); the AgentOS A2A interface lacking authentication by default (CWE-306); a second run_shell finding on argument injection escaping the base_dir restriction (CWE-88); and Team delegation running member agents with the leader’s identity and unrestricted toolset, without scoping or confirmation (CWE-250). A VulDB entry matching that last one appears in search indexes under the identifier CVE-2026-108820; we could not read the VulDB record (the site returned 403 to us) and NVD had no such record when we checked, so we record it as in flight and claim nothing about its contents.
The delegation and orchestration items are the ones we would watch. They are not sink bugs; they are the question of whether a framework’s default assembly — leader delegates to member, member holds the dangerous tool, nothing pauses — constitutes a security boundary at all. That is the same argument this batch just resolved three different ways in three different files.
What to do, with versions that actually exist
Unusually for the disclosures this site has been tracking — PraisonAI’s 24 advisories against a release that was never published, JeecgBoot’s fix that exists only on main, TRAMP’s patch that lives on a mailing list — one third of this batch has a real, installable remedy:
- CodingTools: upgrade to 3.0.10 or later (current release is 3.1.2, 8 October). Note the behaviour change:
run_shellis now opt-in, so agents that relied on it needCodingTools(enable_run_shell=True). Restricted mode now blocks inline interpreter execution and runs without a shell, but the allowlist still containspip,pip3,gitandpytest, and the interpreter check covers thepythonprefix only — which is why the maintainers’ own new docstring saysrestrict_to_base_dir“is NOT a security sandbox.” Treat that sentence as the operative guidance; it is accurate. - PythonTools: there is no patch, and there is no version in which
safe_globalsrestricts anything. Gate the tools exactly as agno’s new docstring recommends —PythonTools(requires_confirmation_tools=["run_python_code", "save_to_file_and_run"])— or drop the execution tools withexclude_tools, or move execution into a real sandbox process. Do not passsafe_globals={}and assume it did anything. - AgentOS: set
os_security_key(orauthorization=True, or the JWT environment variables) before the service reaches any interface you do not control, and check/infofrom an unauthenticated client: if it answersauth_mode: "none", so will everything else. Enforce authentication at the proxy as well, because the application layer is designed to permit when unconfigured.
The ledger here is better than most weeks and still uneven. A framework at 42,000 stars answered three same-day reports with a thorough fix, an honest paragraph and silence, and the catalogue then scored the arbitrary-code-execution findings below the one that only exposes the server. The pattern worth carrying forward is not “agno is unsafe.” It is that in agent frameworks the dividing line between “vulnerability” and “documented behaviour” is currently drawn by whichever side writes the sentence first — and the only one of these three decisions that changed what a default deployment does was the one that shipped a flag.
Verification note. The three CVE identifiers, publication timestamps (09:16:43Z, 12 October 2026), CVSS 3.1 and 4.0 scores and vectors, CWE assignments, affected and fixed version strings, vulnStatus values and the assigning source cna@vuldb.com were read first-hand from the NVD 2.0 API, as was CVE-2026-37003. Issue states, comment text and timestamps for #10047, #10048 and #10051, pull-request titles and merge times for #10210, #10218 and #10220, the commit diff of ed64fab5, release tags and dates, star and fork counts, and the advisory repository listing were read first-hand from the GitHub API. All source excerpts — python.py and coding.py and os/auth.py at main HEAD and at tags v3.0.6 and v3.1.2 — were read directly from the repository at those revisions and are quoted verbatim. The researcher’s mechanism descriptions and self-assigned 9.8 scores are their stated analysis, read first-hand from their advisories and issues and attributed as such, not independently re-derived. We did not install agno, execute any proof of concept, start an AgentOS instance, or send a request to any third-party deployment. The CVE-2026-108820 identifier is reported from a search-index title only; the VulDB record returned 403 and no NVD record existed at the time of writing. The comparison between the three maintainer responses, the scoring-incoherence argument and the composition reading are our editorial analysis. We have no evidence of exploitation of any of these and imply none.
Sources:
- NVD — CVE-2026-108816, agno PythonTools
run_python_codecode injection (6.3 / 2.1, CWE-94, no fixed version) - NVD — CVE-2026-108817, agno CodingTools
run_shellOS command injection (5.5 / 2.0, CWE-78, fixed in 3.0.10) - NVD — CVE-2026-108818, agno AgentOS missing authentication (7.3 / 5.5, CWE-306, no fixed version)
- NVD — CVE-2026-37003, agno through 2.5.8 RCE via prompt injection into
exec()/runpy/subprocess(9.8, August 2026) - agno issue #10047 — PythonTools
run_python_codereport, open since 8 September 2026 - agno issue #10048 — CodingTools
run_shellreport, closed 16 September 2026 with the PoC re-run against the merged fix - agno issue #10051 — AgentOS no-authentication-by-default report, open with no comments
- agno commit
ed64fab5(PR #10210) — hardenrun_shellagainst interpreter RCE,enable_run_shelldefaulted off - agno PR #10218 — “fix: document PythonTools has no code sandbox”, merged 16 September 2026
- agno
libs/agno/agno/tools/python.pyatmain— the unchangedsafe_globals or globals()initialisation - agno
libs/agno/agno/os/auth.pyatmain— the fail-open authentication dependency - ybyu-ieu/agno-security-advisories — the seven advisories, three of which now carry CVEs