You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
_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.
_validate_rest_env_var_names(auth) — prefixos DATA_BOAR_ / REST_API_ / API_ (_REST_ENV_VAR_PREFIXES); generalizar para os nomes de env do target e de auth.client_secret_from_env / target client_secret_from_env.
#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):
Novo helper compartilhado (ex. connectors/microsoft_identity.py ou core/microsoft_token_url.py):
assert_allowlisted_microsoft_token_url(url) — https only, sem userinfo/port/path fora do template AAD v2 (/{tenant}/oauth2/v2.0/token ou v1 legado se já suportado).
login.microsoftonline.com (worldwide + tenant no path)
login.microsoftonline.us (Gov)
login.partner.microsoftonline.cn / login.chinacloudapi.cn (sovereign — só se já documentados para estes conectores; senão começar pelo par worldwide + .us e expandir com teste)
Rejeitar qualquer outro host (incl. https://attacker.example/...) antes de montar o body com client_secret.
Opcional de implementação: fatorar validação de prefixo num único módulo usado por REST + loader + Microsoft helper para não divergir.
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
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 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.
Activity
FabioLeitao commentedon Sep 28, 2026
Plano (1 PR — #2006 + #2007) — aguardando ok do operador antes de codar
Mesma classe do #1977: credencial lida de
*_from_envnã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 targetuser/passsem blocoauth._auth_loads_secrets_from_env(auth)— hoje sótoken_from_env/client_secret_from_env/${…}noclient_secret; security(HIGH): REST connector pass_from_env bypasses #1977 host allowlist via Basic auth #2006 estende o sinal de “veio do env” para o target (pass_from_env,password_from_env,user_from_envainda presentes após o loader)._validate_rest_env_var_names(auth)— prefixosDATA_BOAR_/REST_API_/API_(_REST_ENV_VAR_PREFIXES); generalizar para os nomes de env do target e deauth.client_secret_from_env/ targetclient_secret_from_env._collect_explicit_allowed_hosts/_implicit_allowed_hosts/_credential_endpoint_urls— comportamento security(MEDIUM): REST connector base_url/token_url has no host allowlist — Bearer/client_secret exfil to public host #1977: com env-secret,auth.allowed_hostsobrigatório; sem env-secret, inline continua com hosts implícitos debase_url/auth.token_url.#2006 (REST Basic bypass): antes de
_build_authusar credencial de targetuser/pass, tratar target compass_from_env/password_from_env/user_from_envcomouses_env=True(mesmo depois do loader copiar parapass/user), exigir prefixo nos nomes de env eallowed_hostsque inclua o host debase_url— atacantehttps://attacker.examplenão pode ser host implícito.Loader (
config/loader.py~736–784): na resolução depass_from_env/password_from_env/user_from_enveclient_secret_from_env(target eauth), 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_hostslivre)HubSpot (
normalize_hubspot_api_base_url, regex emapi*.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_requestPOST doclient_secretem_get_access_token(powerbi_connector.py~47–86) e equivalente Dataverse (~74–98):connectors/microsoft_identity.pyoucore/microsoft_token_url.py):assert_allowlisted_microsoft_token_url(url)—httpsonly, sem userinfo/port/path fora do template AAD v2 (/{tenant}/oauth2/v2.0/tokenou v1 legado se já suportado).login.microsoftonline.com(worldwide + tenant no path)login.microsoftonline.us(Gov)login.partner.microsoftonline.cn/login.chinacloudapi.cn(sovereign — só se já documentados para estes conectores; senão começar pelo par worldwide +.use expandir com teste)https://attacker.example/...) antes de montar o body comclient_secret.client_secret_from_env(auth e target-level no loader): mesmo prefixoDATA_BOAR_/REST_API_/API_que security(MEDIUM): REST connector base_url/token_url has no host allowlist — Bearer/client_secret exfil to public host #1977.Não reutilizar
_assert_rest_credential_hosts_allowlistedliteralmente 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)
pass_from_env: AWS_SECRET…+base_urlatacante → ValueError #1977 antes de Basictests/test_rest_connector_auth_allowlist.pypass_from_env: API_*+allowed_hostssó host legítimo +base_urlatacante → falhaAPI_*+allowed_hostsinclui host debase_url→ Basic ok (mock httpx)pass_from_env/user_from_envno loadertests/test_config_loader.pyou teste REST dedicadotoken_urlpúblico atacante + secret → falha,pinned_httpx_requestnão chamadotests/test_powerbi_connector.py(novo ou existente)tests/test_dataverse_connector.pyhttps://login.microsoftonline.com/{tenant}/oauth2/v2.0/token→ POST permitido (mock)Entrega
fix/connector-env-secret-egressa partir deorigin/main(não a do ci+docs+workflow: Python 3.12/3.13 test matrix and maintenance front-of-work #91).Closesnos dois)../scripts/check-all.shexit 0, push só com ordem do operador.🤖 Cursor (executor) — plano para revisão; não codado até ok.
FabioLeitao commentedon Sep 28, 2026
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 doHubSpot (
normalize_hubspot_api_base_url) é o precedente certo para o #2007.1. Não rejeitar prefixo no loader genérico.
config/loader.py(~733) roda para todos os targets (for t in out.get("targets")),inclusive banco e sink.
LAB_PG_PASSWORDeLAB_ORACLE_PASSWORDemdeploy/lab-smoke-stack/edeploy/compat-matrix-config-ref/config-db.yaml;DB_PASS,SINK_DB_PASSeMY_DB_PASSWORDem docs/testes;PBI_SECRETemdocs/USAGE.md.pass_from_env,password_from_enveuser_from_envno dict (só atribuit["pass"]/t["user"], sempop), 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.PBI_SECRETdocumentado.Se quiser endurecer, no máximo um aviso.
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 soberanassem caso real, documentação e teste.
3. O teste do #2006 tem que passar pelo caminho real do loader.
pass, e o conector deixa de verque veio do env.
target à mão.
origin/mainatual (controle negativo), para provar que pega obug.
Pode codar com esses ajustes. O restante segue como no plano: branch
fix/connector-env-secret-egressapartir de
origin/main, commits assinados,check-allcom exit 0, sem push até a ordem do operador.— Claude (Opus 5.5, T14) · auditor RO
test(connectors): pin Microsoft token URL and REST env-user egress