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:
- query strings de URL (
?...apóshttp/https/ws/wss/ftp) pelo marcador; - credencial no componente userinfo de uma URL (
esquema://algo@host, com ou sem:senha) pelo marcador: o userinfo inteiro viraesquema://[REDACTED]@host, porque o usuário faz parte da credencial; - 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; - valores de atribuições sensíveis (
password,passwd,secret,token,api_key,access_key,client_secret,private_key,credentialseguidos 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 JWTeyJ….….…, corpo DERMII…, 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); - formas de credencial conhecidas (
AKIA…,sk-…,gh[pousr]_…, e desde o AUR-609 JWT, corpo DER em base64MII…, SendGridSG.…, Slackxox…e Discord) e o bloco de chave privada inteiro pelo marcador: do bannerBEGIN …PRIVATE KEYaté o bannerEND …PRIVATE KEY, inclusive. Num texto redigido de uma vez o bloco inteiro é substituído; numWriter(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; - valores de segredo registrados — inclusive
AURUM_SECRET_CANARYviaFromEnv— pelo marcador, como defesa em profundidade, mesmo quando o valor foi fatiado por quebras de linha: oWriterjunta 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
Writernão tem teto por linha (issue #179): uma linha de qualquer tamanho é acumulada até o fim (quebra de linha ouFlush) e redigida inteira, sem recusa nem descarte, então um segredo partido entre escritas é visto inteiro.MaxInputBytes = 65536vale só paraRedactBounded, que recusa com*LimitErrortipado o valor acima dele. - Vetores selados: no máximo 64 vetores, 4 MiB expandidos, deadline de 30 s.
redactionsuite.Loadrecusa o que exceder com*SpecErrortipado (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_countmedido — as linhasAUR009_LEAKdo sweep mais qualquer ocorrência do canário nos logs de camada capturados — e derivasecret_outside_authorized_sinkdessa 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
AUR-009/AC-001/behavior-missing: o filtro não redigiu os vetores; o log da camada de integração listaAUR009_LEAK/AUR009_MISMATCHpor canal (nunca o conteúdo cru).AUR-009/AC-001/MUT-001: somente o canalurldivergiu sem vazamento — assinatura da redação de query string desabilitada.AUR-009/AC-001/MUT-002: somente o canalerrorvazou — assinatura de canário injetado no erro tipado.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.AUR-009/AC-001/inconclusive/*(exit 79): ferramenta ausente ou falha de infraestrutura; nunca é RED nem GREEN.- Falha de compilação/loader aparece como
integration_exit:Nouunit_exit:Ne invalida o RED — não é evidência comportamental.