Skip to content

[Security] AgentOS component config resolution returns backend DB credentials (db_url w/ password) to components:read callers #8706

Description

@bogdancherniy11-sudo

Summary

_resolve_db_in_config() expands a component's db id-reference into the fully-resolved
resolved_db.to_dict(), which for PostgresDb/SqliteDb includes db_url — and a Postgres URL
embeds the password. That expanded block is persisted and returned verbatim by
GET /components/{id}/configs/current | /configs/{version} | /configs, gated only by
components:read/components:write (NOT admin/config:read). A low-privilege Studio/components
principal thus recovers the backend DB master credentials.

Affected code (main @ 07fe6b23)

  • os/routers/components/components.py _resolve_db_in_config: config["db"] = {**resolved_db.to_dict(), **table_overrides}.
  • db/postgres/postgres.py:170-179 to_dict() → includes "db_url": self.db_url (password in URL).
  • db/sqlite/sqlite.py:158-163 to_dict() → includes db_url/db_file.
  • GET /components/*/configs* returns ComponentConfigResponse.config verbatim under components:read.

Root cause

The DB to_dict() used for the stored/returned config carries the secret db_url; the resolve step
bakes it into the component config; components:read is not an admin scope. Secrets round-trip to
non-admin callers.

Preconditions / attacker model

  • A principal with components:read (Studio surface). Confirmed 403 on /config (genuinely low-priv).
    Unauthenticated in default no-OS_SECURITY_KEY deployment. Pre-existing Studio configs already carry
    resolved creds, so even read-only suffices.

Impact — HIGH

DB master password disclosure → full data-plane compromise (sessions, memories, the auth_tokens
table, all tenants) and lateral movement to the DB host.

Reproduction (mechanism)

POST /components referencing a db by id ({"db":{"id":"prod-postgres"}}), then
GET /components/{id}/configs/current with a components:read token → response config.db.db_url
contains postgresql+psycopg://<user>:<password>@host:5432/db. Local PoC:
/mnt/ntfs-p2/agno-hunt-2026-07-02/poc/agentG/poc-1.py (fake creds). Captured:

GET /components/my-studio-agent/configs/current -> 200
config.db.db_url = postgresql+psycopg://agno_admin:SuperSecretDbPassw0rd@prod-db.internal:5432/agno_prod
db_url leaked to components:read token = True

Expected vs actual

  • Actual: resolved component config contains connection secrets (db_url w/ password), returned to
    components:read.
  • Expected: component config responses never contain connection secrets; the db stays a non-secret
    id reference (or secrets are redacted).

Suggested fix

Do not embed the secret-bearing db_url in the stored/returned component config. Options:

  1. Keep db as {"id": ..., <table overrides>} in persisted/returned config; resolve the live db
    (with its db_url) only in-memory at load time — never persist/return the expansion.
  2. Add to_dict(redact_secrets=True) (drops db_url/password) and use it for the resolve/response path.
  3. At minimum, strip db_url/credentials from ComponentConfigResponse.config before returning.

Duplicate / publicness (GitHub API, this session)

No issue/PR/GHSA reports component-config credential disclosure (db_url searches: 0). #8477 covers
NON-secret model connection params only (Model.to_dict), not db_url. Related to #8702 (same
function, injection vector); distinct impact.

Environment tested

  • agno 2.6.20, main @ 07fe6b2, Python 3.12. Local-only reproduction; fake creds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions