Pular para conteúdo

AUR-009 — redigir segredos em todos os sinks

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Contrato

internal/security/redaction é o filtro único de redação do AurumCode. Todo sink de processo — stdout, stderr, log, finding, diff, cache e evidence — escreve através desse filtro, e nenhum outro nome de sink é construível: NewWriter recusa qualquer nome fora dos sete canônicos com um *SinkError tipado antes de existir um writer. O filtro troca, nesta ordem e antes de qualquer escrita:

  1. query strings de URL (?... após http/https/ws/wss/ftp) pelo marcador;
  2. credencial no componente userinfo de uma URL (esquema://algo@host, com ou sem :senha) pelo marcador: o userinfo inteiro vira esquema://[REDACTED]@host, porque o usuário faz parte da credencial;
  3. valores de headers de autenticação e sessão (Authorization, Proxy-Authorization, X-Api-Key, Api-Key, X-Auth-Token, X-Amz-Security-Token, Cookie, Set-Cookie) pelo marcador — tanto na forma de linha ancorada (Authorization: …) quanto embutida em payload serializado ({"headers":{"Authorization":"…"}}), que é a serialização dominante num sistema de agentes;
  4. valores de atribuições sensíveis (password, passwd, secret, token, api_key, access_key, client_secret, private_key, credential seguidos de =, : ou :=) pelo marcador. Um valor citado ("…", '…') é literal e é sempre trocado; as aspas são preservadas para o payload continuar parseável. Um valor nu termina em espaço, ", ', ,, ;, & ou } e também é trocado, por mais fraco ou curto que seja (password=hunter2, token = tok), a menos que a expressão inteira que ele começa seja código (AUR-609), numa gramática ancorada: chamada por uma cadeia opcional de membros (os.environ.get("API_KEY"), Config::token()) ou referência inteira (${{ secrets.LLM_API_KEY }}, $VAR, ${VAR}, $(comando)). Cadeia de membros e identificador são trocados inteiros e a linha continua válida (apiKey = cfg.APIKey → apiKey = [REDACTED]). Um byte de código no meio de um segredo não basta: Tr0ub4dor.9xQ, p@ss(1), abc$def, abc.def123, [hunter2] e ${X:-literal} são trocados. Forma conhecida (as do item 5, mais JWT eyJ….….…, corpo DER MII…, SendGrid, Slack e Discord) é trocada em qualquer lugar (item 5), inclusive dentro de chamada mantida. Colchete só pareia na mesma linha, e uma chamada com colchete ou aspa sem par é trocada; numa chamada mantida, as chaves dos argumentos são redigidas também (token=build(secret=[REDACTED])). A redação é linear no tamanho do texto: o valor e os pares de colchete vêm de tabelas montadas numa passada. Risco residual: um valor que, inteiro, tem forma de chamada (Passw0rd(1)) ou de referência ($ecret1) passa; o scanner de segredos, que lê o diff bruto, continua vendo. O separador := é lido inteiro e um valor nu nunca começa por = ou :, então o segundo = de ==/=== é operador, não valor (if token == nil { passa intacto);
  5. formas de credencial conhecidas (AKIA…, sk-…, gh[pousr]_…, e desde o AUR-609 JWT, corpo DER em base64 MII…, SendGrid SG.…, Slack xox… e Discord) e o bloco de chave privada inteiro pelo marcador: do banner BEGIN …PRIVATE KEY até o banner END …PRIVATE KEY, inclusive. Num texto redigido de uma vez o bloco inteiro é substituído; num Writer (que é orientado a linha) o banner de abertura é substituído e todas as linhas seguintes, até o banner de fechamento inclusive, são suprimidas e nunca alcançam o sink;
  6. valores de segredo registrados — inclusive AURUM_SECRET_CANARY via FromEnv — pelo marcador, como defesa em profundidade, mesmo quando o valor foi fatiado por quebras de linha: o Writer junta o segredo enquanto as duas metades ainda estão no buffer e retém o fragmento final que ainda poderia completar um segredo registrado, de modo que nenhuma metade verbatim é escrita.

O marcador estável é [REDACTED]. Erros seguem o mesmo contrato: WrapError devolve um *RedactedError cuja mensagem já foi redigida e que não expõe a causa crua, porque um erro escapa das fronteiras dos writers. A custódia de credenciais permanece fora do AurumCode: este pacote só impede o vazamento nos sinks (non-goal explícito do card).

Exemplo sem segredo (executado pelo aceite)

O exemplo abaixo é o vetor url-query-canary de tests/specs/AUR-009/cases.yaml; o mesmo aceite que valida este documento executa o vetor em todos os sete sinks. ${CANARY} é expandido em runtime para o valor de AURUM_SECRET_CANARY e nunca é um literal comprometido.

Entrada:

GET https://api.example.test/v1/repos?access_token=${CANARY}&page=2 HTTP/1.1

Saída em qualquer sink:

GET https://api.example.test/v1/repos?[REDACTED] HTTP/1.1

O mesmo vale para headers: Authorization: Bearer ${CANARY} torna-se Authorization: [REDACTED], e a mesma troca acontece com o header embutido em JSON ({"headers":{"Authorization":"…"}}), com o valor citado de uma atribuição sensível ({"password":"…"} e api_key: "…") e com o userinfo de uma URL (https://admin:…@git.example.test/r.git → https://[REDACTED]@git.example.test/r.git). Cada uma dessas formas é um vetor selado de tests/specs/AUR-009/cases.yaml, executado nos sete sinks pelo mesmo aceite.

Limites

  • O Writer não tem teto por linha (issue #179): uma linha de qualquer tamanho é acumulada até o fim (quebra de linha ou Flush) e redigida inteira, sem recusa nem descarte, então um segredo partido entre escritas é visto inteiro. MaxInputBytes = 65536 vale só para RedactBounded, que recusa com *LimitError tipado o valor acima dele.
  • Vetores selados: no máximo 64 vetores, 4 MiB expandidos, deadline de 30 s. redactionsuite.Load recusa o que exceder com *SpecError tipado (cases-too-many, cases-too-large, limits-out-of-contract), e o acceptance em bash impõe os mesmos limites antes de compilar qualquer teste.
  • Replay: a mesma spec e o mesmo canário reproduzem exit code, ordem e digest do sweep; divergência falha a integração.
  • O artefato do aceite reporta observed_leak_count medido — as linhas AUR009_LEAK do sweep mais qualquer ocorrência do canário nos logs de camada capturados — e deriva secret_outside_authorized_sink dessa contagem. Uma contagem maior que zero falha o aceite; o campo nunca é um literal do relatório.

Camadas executáveis

  • TestAUR009 (tests/unit/AUR-009.go) prova cada regra do filtro — inclusive as formas citadas, o header em JSON, o userinfo de URL e o bloco de chave privada com valores que o segredo registrado não resgata —, o writer com escrita fatiada com e sem quebra de linha, os erros tipados e os sete sinks canônicos.
  • IntegrationAUR009 (tests/integration/AUR-009.go) executa os vetores selados em todos os sinks, prova a rejeição dos limites do harness e o replay determinístico.
  • E2EAUR009 (tests/e2e/AUR-009.sh) invoca o acceptance pelo selector de integração e exige o marcador de sucesso.
  • Contract: não aplicável — nenhum port ou wire contract é criado.

Diagnóstico

  1. AUR-009/AC-001/behavior-missing: o filtro não redigiu os vetores; o log da camada de integração lista AUR009_LEAK/AUR009_MISMATCH por canal (nunca o conteúdo cru).
  2. AUR-009/AC-001/MUT-001: somente o canal url divergiu sem vazamento — assinatura da redação de query string desabilitada.
  3. AUR-009/AC-001/MUT-002: somente o canal error vazou — assinatura de canário injetado no erro tipado.
  4. AUR-009/AC-001/secret_canary_exposed: um log de camada conteria o canário; o conteúdo é suprimido e a execução falha fechada.
  5. AUR-009/AC-001/inconclusive/* (exit 79): ferramenta ausente ou falha de infraestrutura; nunca é RED nem GREEN.
  6. Falha de compilação/loader aparece como integration_exit:N ou unit_exit:N e invalida o RED — não é evidência comportamental.