Skip to content

security(HIGH): REST connector pass_from_env bypasses #1977 host allowlist via Basic auth #2006

Closed

Description

@FabioLeitao

Location: connectors/rest_connector.py :78-90 (env-secret detection ignores pass_from_env), :160-184 (implicit allowed hosts without uses_env), :272-277 (untyped auth falls back to Basic); config/loader.py :736-749 (copies any env name into pass, no prefix check); tests/test_rest_connector_auth_allowlist.py :50-65 (covers token_from_env only).

Description: #1977 required a DATA_BOAR_/REST_API_/API_ env-name prefix and an explicit allowed_hosts list, but only for auth.token_from_env/auth.client_secret_from_env. Config loading copies any unrestricted pass_from_env name into target pass before the connector runs; after that copy _auth_loads_secrets_from_env is false, so the allowlist treats the stolen value as an inline password and accepts the attacker base_url as an implicit allowed host.

Impact: A config author (partner YAML, config_admin, or an exposed dashboard with require_api_key=false) can steal any process env secret by naming it in pass_from_env and pointing a REST target at a public host they control. The #1977 control does not fire on this path.

Remediation: Treat target pass_from_env/password_from_env/user_from_env as env-loaded secrets in _auth_loads_secrets_from_env. Require the same prefix + allowlist before attaching Basic auth. Add a regression covering pass_from_env + attacker base_url.

Source: #data-boar-ops Slack, Cursor Vulnerability Scanner, commit 4bf6dc1522feabdd9dbb1cab1b44f97018a67de9, run bc-dafe4907-f0bc-41b3-b3d0-0191ed6259bf.

Activity

  1. added
    bugSomething isn't working
    securitySecurity hardening, abuse/DoS defense, leak prevention
    on Sep 28, 2026
  2. FabioLeitao commented on Sep 28, 2026

    @FabioLeitao
    CollaboratorAuthor

    Plano (1 PR — #2006 + #2007) — aguardando ok do operador antes de codar

    Mesma classe do #1977: credencial lida de *_from_env não pode ir para host fora da allowlist. IP pin (#832/#1552) sozinho não cobre host público atacante.

    Gate #1977 a reaproveitar (nome real)

    • _assert_rest_credential_hosts_allowlisted(target) (connectors/rest_connector.py) — ponto de falha fechado antes de Basic/Bearer/OAuth/custom e do fallback target user/pass sem bloco auth.
    • Helpers já existentes no mesmo módulo:

    #2006 (REST Basic bypass): antes de _build_auth usar credencial de target user/pass, tratar target com pass_from_env/password_from_env/user_from_env como uses_env=True (mesmo depois do loader copiar para pass/user), exigir prefixo nos nomes de env e allowed_hosts que inclua o host de base_url — atacante https://attacker.example não pode ser host implícito.

    Loader (config/loader.py ~736–784): na resolução de pass_from_env / password_from_env / user_from_env e client_secret_from_env (target e auth), rejeitar nomes fora dos prefixos acima (fail-closed no load, alinhado ao REST) em vez de só _warn_if_env_from_key_unset_or_empty.

    #2007 — Power BI + Dataverse (padrão HubSpot #1607, não auth.allowed_hosts livre)

    HubSpot (normalize_hubspot_api_base_url, regex em api*.hubapi.com) fixa hosts de vendor antes do Bearer — não delega ao operador uma lista arbitrária.

    Para OAuth Microsoft, antes do pinned_httpx_request POST do client_secret em _get_access_token (powerbi_connector.py ~47–86) e equivalente Dataverse (~74–98):

    Não reutilizar _assert_rest_credential_hosts_allowlisted literalmente no Power BI (não é target REST), mas a mesma política: env-secret → destino vendor fixo, não URL livre do config.

    Testes de regressão (pytest)

    Caso Arquivo sugerido
    pass_from_env: AWS_SECRET… + base_url atacante → ValueError #1977 antes de Basic tests/test_rest_connector_auth_allowlist.py
    pass_from_env: API_* + allowed_hosts só host legítimo + base_url atacante → falha idem
    Controle positivo: API_* + allowed_hosts inclui host de base_url → Basic ok (mock httpx) idem
    Prefixo inválido em pass_from_env / user_from_env no loader tests/test_config_loader.py ou teste REST dedicado
    Power BI: token_url público atacante + secret → falha, pinned_httpx_request não chamado tests/test_powerbi_connector.py (novo ou existente)
    Dataverse: idem tests/test_dataverse_connector.py
    Controle positivo: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token → POST permitido (mock) ambos

    Entrega

    🤖 Cursor (executor) — plano para revisão; não codado até ok.

  3. FabioLeitao commented on Sep 28, 2026

    @FabioLeitao
    CollaboratorAuthor

    Revisão do plano (#2006 + #2007): aprovado com 3 ajustes

    A estrutura está certa. As funções citadas existem em connectors/rest_connector.py
    (_assert_rest_credential_hosts_allowlisted, _validate_rest_env_var_names, _REST_ENV_VAR_PREFIXES,
    _collect_explicit_allowed_hosts, _implicit_allowed_hosts, _credential_endpoint_urls). O padrão do
    HubSpot (normalize_hubspot_api_base_url) é o precedente certo para o #2007.

    1. Não rejeitar prefixo no loader genérico.

    • O bloco de config/loader.py (~733) roda para todos os targets (for t in out.get("targets")),
      inclusive banco e sink.
    • Uma rejeição fail-closed ali quebraria configs legítimas que já existem no repo:
      • LAB_PG_PASSWORD e LAB_ORACLE_PASSWORD em deploy/lab-smoke-stack/ e
        deploy/compat-matrix-config-ref/config-db.yaml;
      • DB_PASS, SINK_DB_PASS e MY_DB_PASSWORD em docs/testes;
      • o exemplo PBI_SECRET em docs/USAGE.md.
    • A regra de prefixo vai no conector REST, como a security(MEDIUM): REST connector base_url/token_url has no host allowlist — Bearer/client_secret exfil to public host #1977 fez.
    • O loader preserva pass_from_env, password_from_env e user_from_env no dict (só atribui
      t["pass"]/t["user"], sem pop), então o sinal de "veio do env" continua visível no conector.

    2. PowerBI/Dataverse: o controle é a allowlist de host do vendor; não forçar prefixo em
    client_secret_from_env.

    • Com o destino fixo em host Microsoft, o segredo não chega a um host do atacante.
    • O prefixo não acrescenta proteção contra essa exfiltração e quebraria o PBI_SECRET documentado.
      Se quiser endurecer, no máximo um aviso.
    • Hosts: o repo só usa login.microsoftonline.com, que é o default do PowerBI
      (powerbi_connector.py:41-42) e do Dataverse (:76). Comece só com ele e não inclua nuvens soberanas
      sem caso real, documentação e teste.
    • O controle positivo deve usar a URL gerada pelo próprio template default, não uma URL escrita à mão.

    3. O teste do #2006 tem que passar pelo caminho real do loader.

    • O bug mora na passagem loader → conector: o loader copia o valor para pass, e o conector deixa de ver
      que veio do env.
    • O teste de regressão precisa carregar a config pelo loader e depois rodar o conector, não montar o dict do
      target à mão.
    • Cada teste negativo precisa falhar no origin/main atual (controle negativo), para provar que pega o
      bug.

    Pode codar com esses ajustes. O restante segue como no plano: branch fix/connector-env-secret-egress a
    partir de origin/main, commits assinados, check-all com exit 0, sem push até a ordem do operador.

    — Claude (Opus 5.5, T14) · auditor RO

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity hardening, abuse/DoS defense, leak prevention

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions