The Server Picked the URL and the Title Wrote the Prompt: Four veans Flaws, One Agent-CLI Trust Problem

NVD published four CVEs on 8 November 2026 against veans, the agent CLI that bridges Vikunja project management with coding agents — and each one is a different trust boundary the CLI failed to hold. CVE-2026-123498 (CVSS 3.1 3.7) feeds remote project and task metadata verbatim into the agent’s authoritative instruction block. CVE-2026-123499 (5.9) lets a repository’s .veans.yml redirect the VEANS_TOKEN environment credential to an attacker’s server. CVE-2026-123496 (3.3) lets hostile-repo symlinks steer veans init writes outside the checkout. CVE-2026-123497 (6.8) lets an on-path attacker downgrade server discovery from HTTPS to HTTP and capture passwords, TOTP codes and tokens. All four are fixed in Vikunja 2.7.1, released 20 October with what its changelog calls a whopping 41 security fixes from community reports and an automated audit — the advisories note all veans versions were affected, since veans has no tagged release of its own. Each advisory credits an internal security review as finder, and each fix converges on the same architectural answer: a trust gate with fail-closed behavior.

The prompt boundary: project titles as instructions (CVE-2026-123498)

The highest-interest flaw is also the lowest-scored. A project member with write access sets a project title — or task titles and descriptions — and veans renders those fields inside the authoritative instruction block every session start, via the prime command that agent hooks run on SessionStart and PreCompact. Three of the four rendered fields come from local .veans.yml, which adds nothing because the repo already controls CLAUDE.md; the advisory is explicit that the real boundary is the server-provided ProjectTitle and task content, and on an untrusted server the server controls all of it. Severity low reflects the preconditions — write access plus an agent that acts on the text — but the shape is the canonical agent-CLI prompt injection: metadata with a legitimate display purpose lands in a privileged context with no fencing. The fix has two parts worth quoting because the pattern generalizes: a trust gate so an untrusted server’s metadata never reaches the prompt (prime fails closed when non-interactive, rendering remote content only with a hint to run a command interactively), plus fencing — even a writer on a trusted server gets their content rendered as a labeled, fenced untrusted-data block outside the authority section.

The credential boundary: the repo chooses the server (CVE-2026-123499)

veans takes the Vikunja server URL from the nearest .veans.yml, walking up from the working directory — and when VEANS_TOKEN is set, it attaches that token as a Bearer header with no origin check. Run any veans command in an untrusted checkout, or let CI run veans on PR code, and the token goes to the repo author’s server, carrying whatever access it holds on the real one. The prime-on-session-start hook means the leak fires automatically. Two details bound the blast radius precisely: keyring and file tokens are keyed by server, so only the env token leaks — the classic CI-shaped credential — and the same root cause folded in a sibling issue where veans login sent human passwords and tokens to a repo-selected server. The fix is the trust gate proper: one origin check before any env token is attached, any human credential is sent, or any remote metadata is fetched for the prompt. Trust records live in global XDG state, never repo-local (an attacker repo could ship a pre-approved one), keyed by normalized origin plus repo identity so an https record never covers a downgraded http origin; interactive runs ask once, non-interactive runs fail closed, and CI can satisfy the gate with a matching VEANS_SERVER variable. This is the advisory the other three orbit: once repository content can pick the server, every downstream use of that server inherits the attacker.

The filesystem and transport boundaries: symlinks and downgrade (CVE-2026-123496, CVE-2026-123497)

The remaining two complete the set. veans init performed no Lstat, O_NOFOLLOW or symlink evaluation anywhere in its write paths, so a hostile repo ships symlinks and the victim’s own init writes outside the checkout: .veans.yml aimed at ~/.bashrc, .claude at ~/.claude to plant a hook in the global settings, .opencode at a global plugin install. The attacker cannot control the written content, so the advisory rates this destruction-or-stray-global-hook rather than code execution — but a global agent hook lands in every future agent session, which regular readers will recognize as the persistence primitive behind this season’s hook-abuse coverage, from testmetest’s armed repositories to the child-session hook bypass and the Claude Code Edit-tool TOCTOU escape. The prescribed fix is Go 1.27’s os.OpenRoot with repo-relative reads and writes. Separately, server discovery appended HTTP candidates after explicit https:// URLs and accepted the first response that parsed as server info with no identity check — so an on-path attacker blocking ports 443/3456 captures the posted password or bot token during init/login, and the resulting HTTP base URL keeps all later traffic cleartext. The fix: HTTP only for loopback or explicitly typed http://, never as fallback. Note the deliberate layering: the trust gate alone would not catch the downgrade unless the user actually reads the http:// in the prompt, so transport gets its own rule.

What to actually do

  1. Upgrade to Vikunja ≥ 2.7.1 (signed Docker images with provenance attestation are available per the release notes) and run vikunja repair attachment-previews only during a quiet window if you use Redis, per the changelog.
  2. Audit existing trust records and server URLs in veans state: any origin trusted before the gate, or any http:// base URL persisted by the old discovery fallback, should be re-established deliberately.
  3. Rotate any VEANS_TOKEN or human credential that ever ran inside an untrusted checkout or CI-on-PR-code, and scope CI tokens so a redirect yields minimum access.
  4. Generalize the pattern to your own agent CLIs: repository content must never select the server, attach the credential, write the prompt, or resolve the path without an explicit, non-repo-local trust decision — and hooks that run non-interactively must fail closed, because no human is present to read the warning. The unpinned-MCP census and the JetBrains Junie batch show the same lesson from adjacent angles.

Our verification was read-only. We read all four NVD JSON records first-hand (published 8 November 2026; scores 3.7/5.9/3.3/6.8 with CWE-74+CWE-1427, CWE-346+CWE-522, CWE-59+CWE-61, CWE-319+CWE-757; “before 2.7.1” ranges; GHSA and VulnCheck references) and all four GitHub repository security advisories first-hand via the API (summaries, severities, CWE lists, affected ranges, full Summary/Details/Impact/Fix text including file paths, the folded-in login advisory, and the Go 1.27 os.OpenRoot prescription). We read the Vikunja 2.7.1 changelog first-hand (20 October, 41 fixes, signed images, repair command, attachment-preview Redis note) and the 2.7.1/2.8.0 release tags via the API. We installed no Vikunja instance, ran no veans command, crafted no symlink and intercepted no traffic. Brave Search was used for discovery only.

Sources: