Skip to content

fix(api): scope and expire sync tokens (GHSA-2452-4qrm-6p4w) - #16485

Closed
diegosouzapw wants to merge 3 commits into
release/v3.8.52from
fix/ghsa-2452-sync-token-scope
Closed

diegosouzapw wants to merge 3 commits into
release/v3.8.52from
fix/ghsa-2452-sync-token-scope

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Contexto

Corrige o advisory privado GHSA-2452-4qrm-6p4w (reportado por @gsmarcil), severidade média.

GET /api/sync/bundle (rota pública, protegida só pelo sync token) devolve o bundle completo de configuração: credenciais decifradas de todas as conexões de provider (apiKey/accessToken/refreshToken/idToken) e todas as API keys do OmniRoute em texto puro. Até agora:

  • qualquer API key com escopo manage emitia um sync token via POST /api/sync/tokens, sem sessão do dashboard e sem senha;
  • o token não expirava;
  • revogar ou apagar a API key emissora, ou trocar a senha de admin, não o invalidava.

Isso contornava controles que já existem: GET /api/providers mascara os segredos, GET /api/keys/{id}/reveal responde 403 quando o reveal está desligado e POST /api/cloud/auth entrega só fragmentos a chaves manage.

O que muda

  1. Quem pode emitir (src/lib/sync/issuerAuth.ts): POST /api/sync/tokens passa a exigir uma credencial de operador:

    • sessão autenticada do dashboard (senha ou OIDC);
    • a credencial local da CLI (loopback, carimbada pelo pipeline de authz ou validada na rota). Com isso omniroute sync tokens create continua funcionando;
    • instância com login desligado (requireLogin=false), onde toda a superfície de gestão já é aberta.

    API key manage/admin sozinha, ou access token oma_, recebe 403. Sem credencial nenhuma, continua 401. GET (listar, só metadados) e DELETE (revogar) seguem com requireManagementAuth.

  2. Expiração: todo token novo tem expiresAt. O padrão é 30 dias. O corpo aceita ttl (30d, 12h, 90m, o formato que a CLI já enviava em --ttl, mas que o servidor ignorava) ou expiresInDays (mesma convenção de /api/cli/tokens), com teto de 365 dias. Um TTL acima disso responde 400.

  3. Vínculo com o emissor:

    • Senha: o token guarda um digest do hash da senha de admin vigente na emissão. Qualquer rotação invalida todos os tokens: dashboard (/api/settings), /api/settings/require-login, omniroute reset-password e setup da CLI. Comparo o hash armazenado em vez de usar sessionsValidAfter porque este só é atualizado pela rota de settings, e o reset pela CLI (o caminho de recuperação quando o admin foi comprometido) não passaria por ele.
    • API key: quando a requisição de emissão traz uma API key junto da credencial de operador, ela é gravada em syncApiKeyId, como já acontecia. Se essa key for revogada, desativada, banida, expirada ou apagada, o token é rejeitado.
  4. Tokens legados: linhas sem vínculo (emitidas antes da correção, possivelmente por uma key manage) são rejeitadas na validação. A migration 206_sync_token_issuer_binding.sql também as marca como revogadas, para que a listagem mostre isso.

  5. API keys no bundle: com o reveal desligado (ALLOW_API_KEY_REVEAL, o mesmo switch do /api/keys/{id}/reveal), a rota /api/sync/bundle mascara o campo key como a listagem de /api/keys já faz (8 primeiros + **** + 4 últimos). A configuração da key continua indo no bundle. Com o reveal ligado, nada muda. As credenciais de provider continuam no bundle, porque replicar a config para outra instância é o propósito da feature. O que fecha essa exposição é a nova barreira de emissão, a expiração e a invalidação.

Migration: 206 (a mais alta no tip release/v3.8.52 é 205). ⚠️ As PRs abertas #16350, #14665, #16439, #16339, #16087 e #14771 também usam o número 206. A que entrar depois precisa renumerar.

Consumidores verificados

  • CLI omniroute sync bundle <out> (bin/cli/commands/sync.mjs): baixa o bundle com o token via --api-key. Continua funcionando com tokens novos. Tokens antigos precisam ser emitidos de novo.
  • CLI omniroute sync tokens create/list/revoke: a emissão usa apiFetch, que no loopback envia x-omniroute-cli-token, e esse caminho é aceito. O --ttl 30d que a CLI já mandava agora é respeitado, e a coluna Expires da listagem passa a ter valor.
  • Cloud sync (src/lib/cloudSync.ts): monta o payload direto com buildConfigSyncEnvelope(), sem passar pela rota. Não é afetado: o mascaramento das keys acontece só na rota HTTP.
  • Não há UI no dashboard nem outro consumidor do bundle no repositório.
  • Docs: docs/guides/FEATURES.md dizia que o bundle tinha só "settings não sensíveis". Corrigi para descrever o conteúdo real e as regras novas. Os espelhos i18n ficam para o pipeline de tradução.

Prova TDD (RED → GREEN)

tests/unit/sync-token-scope-ghsa-2452.test.ts (DATA_DIR temporário, resetDbInstance() no test.after):

  • RED no código sem correção: 8 de 10 falham pelo motivo esperado. A key manage emite (201 !== 403); o bundle segue 200 depois de revogar ou apagar a key emissora e depois de trocar a senha (200 !== 401); não há expiresAt; token legado é aceito; key em texto puro no bundle. Os 2 que passam são os de regressão: sessão de admin emite e baixa o bundle; CLI local emite.
  • GREEN com a correção: 10/10.

Suítes existentes relacionadas, todas verdes e sem alterar asserções: sync-routes (2), sync-bundle (2), db-sync-tokens (8), cli-sync-commands (8), error-message-sanitization (37), api-auth (40), authz/classify (52), authz/public-route-exact-match (8), hard-session-lease-bypass-inventory (3), capture-critical-db-state (7), api-key-reveal-route (8), check-migration-numbering (15), db-migration-version-uniqueness (2), db-migration-runner (26), db-portable-migration-snapshot-16035 (17), db-portable-migration-gating-16035 (2).

npm run typecheck:core limpo. eslint (com as supressões) e prettier limpos nos arquivos alterados.

Pontos para o dono decidir

  • Tokens existentes deixam de valer depois do upgrade e precisam ser emitidos de novo. Foi intencional, porque alguns podem ter sido emitidos por uma key manage comprometida.
  • POST /api/cli/tokens ainda aceita uma key manage para emitir access tokens oma_ com escopo admin. Isso fica fora do escopo deste advisory, mas é o mesmo padrão (key manage escalando para credencial de operador). Por isso os tokens oma_ não são aceitos para emitir sync tokens.
  • Encerrar a sessão do dashboard que emitiu o token não o revoga, para não quebrar sync de longa duração a cada logout. O vínculo é com a senha.

⚠️ base-red inherited: #15306

GHSA-2452-4qrm-6p4w. A sync token unlocks GET /api/sync/bundle, which
returns every provider credential and every OmniRoute API key in
plaintext. Any manage-scoped API key could mint one without a dashboard
session, and the token never expired nor died with its issuer.

- POST /api/sync/tokens now requires an authenticated dashboard session
  or the loopback CLI credential (or a login-disabled instance); a
  manage-scoped API key or oma_ access token alone gets 403.
- Tokens expire (default 30 days, ttl/expiresInDays up to 365 days).
- Tokens are bound to a digest of the admin password hash at mint time:
  any password rotation (dashboard, require-login, CLI reset) kills them.
- A token whose recorded issuing API key is revoked, expired, banned or
  deleted is rejected.
- Legacy tokens without the binding are rejected and revoked by
  migration 206.
- While API key reveal is disabled the bundle masks OmniRoute API keys
  the same way GET /api/keys does. Cloud sync is unaffected.

Co-authored-by: gsmarcil <gsmarcil@users.noreply.github.com>
Comment thread src/lib/sync/tokens.ts Fixed
…issuer narrowing

The binding digested the stored management password with a fast hash; a legacy
install can still hold that password in plaintext, so it now goes through scrypt.
The issuer check used a truthiness test that does not narrow the union without
strictNullChecks, which broke the API route typecheck.
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

@diegosouzapw

Copy link
Copy Markdown
Collaborator Author

Fechando como duplicada: a GHSA-2452 foi corrigida no tip pela #16478 (sessão de dashboard para emitir, expiração e revogação com o emissor). Advisory já fechado.

@gsmarcil

Copy link
Copy Markdown
Contributor

Hi @diegosouzapw — thanks for confirming that #16478 addresses GHSA-2452-4qrm-6p4w.

I understand that #16485 was closed as a duplicate implementation PR. Could you please clarify:

  1. Does fix(security): require a dashboard session to issue sync tokens and expire/revoke them with their issuer #16478 implement the fix for my original security report, with attribution to @gsmarcil retained?
  2. Was the advisory published or closed without publication?
  3. Was a CVE requested from GitHub, as I previously asked? If not, could you please request one, or explain why you consider this finding ineligible?

My concern is preserving attribution for the original discovery and clarifying its CVE status, regardless of which PR delivers the fix.

Thank you.

@diegosouzapw
diegosouzapw deleted the fix/ghsa-2452-sync-token-scope branch October 18, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants