Repository navigation
security(runtime): harden TLS provenance, lifecycle, and public error boundaries - #11742
Conversation
|
Segurando esta PR por ora — não vou mergeá-la nesta rodada. Motivo: #11753 ("refactor(stealth): replace tls-client-node with wreq-js") remove O dono decidiu segurar as duas PRs por enquanto para avaliar manualmente qual caminho seguir (manter tls-client-node pinado/verificado, ou migrar para wreq-js). Nenhuma ação necessária da sua parte — só um aviso da sobreposição. |
|
Obrigado pelo trabalho de hardening de proveniência aqui! Ao processar esta PR junto com sua companheira #11753 (que acabou de ser retargetada para Depois que #11753 for mesclada, a dependência que esta PR protege deixa de existir no código — não há mais binário do |
Babysit — auditoria do candidato
|
| Item | Resultado |
|---|---|
| PR | #11742 — OPEN + DRAFT |
| Auto-merge | desativado (null) |
| Base exata | 091589089cd134a94df9f6cdab9ba562b2cefd18 (release/v3.8.50) |
| Candidato exato | 29dc5427b6abd5a4ff5198759f8cfc2d500ce1aa |
| Merge state atual | estruturalmente MERGEABLE; política BLOCKED enquanto draft/sem aprovação |
| Diff exato | 97 caminhos, +18.409 / -1.941, em 6 commits |
| Escopo | TLS nativo: proveniência, empacotamento, lifecycle e lock; fronteiras públicas de erro/log em executors/handlers/validation, incluindo LM Arena e Moderations/OCR |
| Autoridade da evidência | Git físico no worktree isolado e GitHub da PR. A geração 2026-09-01T21:14:36Z do grafo cobre o checkout principal .51, não este WIP .50, e não é usada como prova negativa deste candidato |
| PR #11753 | alternativa independente em wreq-js para .51; não é dependência desta PR e não foi mesclada aqui |
Loop de correção do CodeQL — 4/5 iterações
Nenhum alerta foi descartado, suprimido ou reclassificado para obter verde; o limite do skill
babysit não foi excedido.
| Iteração | SHA | Evidência | Providência |
|---|---|---|---|
| 1 | b40a9fc3 |
Todos os jobs de linguagem do CodeQL passaram, mas o verdict externo falhou com #967 js/polynomial-redos e #968 js/stack-trace-exposure |
Iniciada correção delimitada, com regressões; nenhum dismiss/suppress |
| 2 | e5d1bbc9 |
Scanner determinístico corrigiu diretamente #967. Build, DAST, Semgrep, todos os jobs de linguagem e o agregador ficaram verdes, mas o endpoint direto ainda mostrava #968 open |
O verde agregado não foi usado como prova de encerramento; a investigação continuou |
| 3 | d66748406f |
A união fechada de falhas públicas e o stream transport passaram pelo buildErrorBody(); revisão estática encontrou que o status ainda admitia 2xx/3xx |
Adicionados casos negativos específicos; a iteração não foi declarada final |
| 4 | 29dc5427 |
Status público LM Arena limitado a inteiro 400..599; sucessos, redirects, valores inválidos/coercivos/hostis caem para 502 |
TDD RED 53/55 -> GREEN 55/55; matriz Arena 124/124; todos os jobs CodeQL e o agregador PASS; #967/#968 diretamente FIXED |
Evidência local no SHA exato
PASS confirmado em 29dc5427:
- TDD da iteração 4: RED
53/55nos dois novos casos; depois GREEN55/55. - Matriz serial Arena / LM Arena:
124/124. npm run typecheck:core.- ESLint escopado com suppressions do repositório: 0 erros.
- Prettier nos caminhos finais alterados.
npm run check:error-helper.npm run check:cycles.git diff --check.- Vitest MCP, execução final: 44 arquivos /
405/405.- A primeira execução atingiu
404/405por timeout isolado no casoauto/glm. - O recorte isolado passou
11/11. - A repetição integral e serial passou
405/405; o primeiro timeout fica registrado como
flake, não apagado do histórico.
- A primeira execução atingiu
Evidência local anterior, sem alteração material pelos dois commits finais de LM Arena:
- TLS serial
47/47; stress isolado1/1em 48,3s. - Perplexity
54/54; Grok78/78; sanitização canônica/ChatCore121/121. - Validação de providers
7/7+7/7; Moderations/OCR24/24; Notion41/41. - Regressão de semaphore/call-log:
1/1. - Scanner de data URL + recorte canônico:
85/85. - Ratchets de tamanho (
prod=4408,tests=5086), secrets (0), any-budget,
tracked-artifacts, docs, route-validation, build-scope e sintaxe MJS (8/8).
Não são promovidos a PASS:
- O agregado TLS paralelo foi
46/47 LOAD-REDsob contenção; o caso de 100 callers passou
isolado e na matriz serial. - O recorte ChatCore amplo foi
4/11 INFRA-REDcom o guard real de pressão de recursos acima de
96,7%; o caso alterado passou isolado.
CI e segurança no SHA exato
| Verificação | Resultado final |
|---|---|
| Change Classification | PASS em 49s |
Fast Production Build / build:release / upload do artefato |
PASS em 18m21s; artefato omniroute-build enviado (4.996.056.168 bytes) |
| DAST smoke | PASS em 8m34s |
| Semgrep | PASS em 3m07s |
| Semgrep Cloud | PASS |
| CodeQL Actions | PASS em 55s |
| CodeQL C/C++ | PASS em 1m00s |
| CodeQL Python | PASS em 59s |
| CodeQL JavaScript/TypeScript | PASS em 48m30s |
| Advanced Security aggregate | PASS |
| Quality Gates fast-path | SKIPPED — sete jobs pulados pelo classificador; não contabilizados como PASS |
| Vitest UI, 285 arquivos / 1.816 testes | PASS — 285/285 arquivos e 1.816/1.816 testes em 1.417,66s, no SHA 29dc5427 e DATA_DIR temporário |
| Reviews e threads acionáveis | CLEAR — nenhuma thread acionável; uma thread CodeQL antiga está resolved e outdated; nenhum review request |
Estado direto dos alertas — a fonte obrigatória para encerrar este loop, independentemente do
verde do agregador:
| Alerta | Estado no candidato final |
|---|---|
#967 — js/polynomial-redos |
FIXED; nenhum dismiss/suppress; a ref final não possui alerta aberto |
#968 — js/stack-trace-exposure |
FIXED; o endpoint direto mudou de open em e5d1bbc9 para fixed após a análise final; nenhum dismiss/suppress |
Consulta adicional por alertas abertos na ref refs/pull/11742/head: lista vazia.
BASE-RED / branch drift — não atribuídos ao candidato
npm run typecheck:noimplicit:core: 36 erros apenas em
open-sse/translator/response/openai-responses/pureHelpers.ts, inalterado e byte-idêntico à base.npm run check:open-sse-typecheck: mismatch do baseline em
src/app/api/v1/models/catalog.ts, com os blobs envolvidos idênticos à base.npm run check:circular-deps: o script chamanode node_modules/.bin/dpdm, masdpdmé um
shell shim; isso é distinto donpm run check:cyclesque passou acima.- Prettier em
docs/i18n/it/README.md,docs/i18n/tr/README.mde
tests/unit/grok-web.test.ts: mesmos hunks vermelhos na base exata. - Teste Claude Web: fatal nativo em
koffiapós as asserções, reproduzido igualmente na base. npm run check:changelog-integrity: o checker vivo compara esta árvore congelada.50com a
linha.51atual e acusa dez bullets da.51;CHANGELOG.mdpermanece byte-idêntico à base e
esta PR adiciona somente o fragmento próprio.
HOLD / não alegado como PASS
- Autorização comercial para a dependência Commons Clause: exige autorização separada, revisão
jurídica ou substituição por transporte permissivo. NOTICE/proveniência não concedem licença. - Scan de licenças de produção a partir de instalação/lockfile limpos; o
node_modules
compartilhado não prova a árvore exata. npm run check:pack-policy:npm pack --dry-runpermaneceu ativo por mais de dez minutos e foi
interrompido, sem veredito.- Pacote e boot reais, Docker Node/Bun e artefatos Electron Linux/Windows/macOS.
- Homologação e deploy.
- Matriz integral
npm run test:unite coverage.
Disposição para o mantenedor
CI material verde e pronto para revisão humana. Os alertas diretos #967/#968 estão corrigidos,
não há thread/review acionável e os checks materiais executados no candidato final passaram. Isso
não libera merge: os HOLDs acima permanecem e esta PR deve continuar aberta e em draft até
decisão humana.
Nenhum merge, auto-merge, tag, release, publicação, homologação ou deploy foi executado ou
autorizado por esta auditoria.
Merge the current release tip into the security hardening branch and reconcile 57 conflicting files. Most of this PR's original scope landed on the tip while it sat: #12506/#12945/ #13635 shipped a stricter public error boundary (allowlist-based `isSafePublicErrorIdentifier`, `errorSanitization.ts`, `errorPathRedaction.ts`, `upstreamErrorResponse.ts`), #12429 migrated the web-cookie TLS transport to wreq-js, and #11754 retired the common ChatGPT Web executor. Those parts are resolved to the tip, which is strictly more restrictive in every case, and the now-dead tls-client provenance stream is dropped (the tip's `tls-client-wreq-residue` guard forbids reintroducing `tls-client-node`). What survives is the part the tip does not cover: - chatCore reads rejection metadata through `getSafeErrorMetadata`, wraps `isLocalStreamLifecycleError`/`formatProviderError` so a hostile Proxy cannot escape the boundary, sanitizes the failure message before call logs and console, projects the failure-usage code through the bounded vocabulary, and sanitizes the upstream body before it reaches the persisted attempt logs. - Perplexity's non-streaming quota/upstream error body sanitizes the upstream message and projects the provider-supplied code (`toPublicPerplexityErrorCode`). - Arena (lmarena) maps every public failure onto a fixed vocabulary instead of echoing upstream text; `lmarena_stream_error` is registered in the public identifier allowlist. - Notion keeps the tip's fail-closed transport (no plain-fetch proxy bypass) and sanitizes the transport error before the response body. - `chatgptWebTools` returns a non-ok buffered response untouched. Tests for superseded behaviour are dropped; the surviving contributions keep their tests, split into `lmarena-public-error-boundary-11742` and `perplexity-web-public-error-boundary-11742` to stay under the test size cap. Prunes three now-stale eslint suppression entries: notion-web.ts no longer has an unused var after this merge, and the two chipotle entries were left behind by the tip's own provider removal (#13913).
… hardening open-sse/handlers/chatCore.ts 6219 -> 6287. This is the one part of #11742 that survived the rebase: sanitizeErrorMessage on the plugin onError hook, on the semaphore-timeout path and on failureMessage before it reaches console.log and the call log, sanitizeUpstreamDetails on the malformed-response log, and getSafeErrorMetadata + try/catch where hostile (Proxy) metadata could throw. That is the LOG boundary, which is broader than Hard Rule #12 (responses). The rest of the PR was dropped as already landed on the tip.
|
Rebaseada de A PR encolheu de 97 para 27 arquivos. Nas três semanas em que ficou aberta, o tip absorveu quase tudo por três caminhos independentes:
A evidência mais direta: Na sanitização, o tip é mais restritivo que esta PR, então ficou o tip. O Resultado: todo o O que sobreviveu e é o valor real desta PR é o endurecimento da fronteira de log, reaplicado à mão sobre a estrutura nova do Também foi reaplicado o que o tip não tinha: sanitizadores do Perplexity sobre a arquitetura nova (a via não-streaming de quota mandava Validação: 61/61 nos testes de sanitização, 200/200 nos de executor — incluindo Três resíduos que merecem PR própria:
|
… boundaries (cheaper-inference#11742) * security(deps): pin and verify tls-client native artifacts * docs(changelog): link tls-client provenance PR * security(runtime): harden TLS and public error boundaries * security(runtime): resolve CodeQL error-boundary findings * security(lmarena): close public stream error boundary * fix(lmarena): normalize public error statuses * chore(quality): rebaseline chatCore.ts for the surviving log-boundary hardening open-sse/handlers/chatCore.ts 6219 -> 6287. This is the one part of cheaper-inference#11742 that survived the rebase: sanitizeErrorMessage on the plugin onError hook, on the semaphore-timeout path and on failureMessage before it reaches console.log and the call log, sanitizeUpstreamDetails on the malformed-response log, and getSafeErrorMetadata + try/catch where hostile (Proxy) metadata could throw. That is the LOG boundary, which is broader than Hard Rule cheaper-inference#12 (responses). The rest of the PR was dropped as already landed on the tip.
Summary
This draft hardens the frozen
release/v3.8.50line around the nativetls-client-nodesupply chain, runtime lifecycle, and public error/log boundaries. It isindependent of other PRs, but it does change provider executors and validation behavior;
the previous narrower scope statement is intentionally superseded.
tls-client-node@0.2.0, records the exact wrapper/upstream licenses and NOTICE, and pins all six supported v1.15.1 native assets to official SHA-256 digestsTHIRD_PARTY_NOTICES.mdthrough npm, standalone, Docker, and Electron assembly paths47/47, including 100 callers and cross-process contention121/121; runtime/spec review CLEAR400..59953/55-> GREEN55/55; final Arena matrix124/124; direct CodeQL #968 fixed54/54.50base already contains a newer census than the supplied dossier snapshotExact branch boundary
091589089cd134a94df9f6cdab9ba562b2cefd18(release/v3.8.50).29dc5427b6abd5a4ff5198759f8cfc2d500ce1aa.open-sse/orsrc/; there are no productionchanges under
electron/orbin/.wreq-jsmigration forrelease/v3.8.51; this PR deliberately keeps the exact.50base and hardens the dependencyshipped there.
boundary. It therefore includes the five Moderation/OCR helper-and-test paths instead of
presenting them as TLS-only work; the title, summary table, and changelog all name that expansion.
Runtime and security changes
Native dependency and lifecycle
copying it.
identity before cleanup, so an older owner does not delete a newer owner's lock.
without retaining native stack/cause data.
Error,AbortError,TimeoutError,BodyTimeoutError,TlsClientHangError, andNativeTlsError; every other, hostile, or credential-shaped value falls back toError.Public errors and persistent logs
canonical error sanitizer.
getPrototypeOftraps fail closed.paths, raw and serialized line separators, and sensitive nested request-log payloads.
Retry-After, SSE framing, public error codes, and internal cooldown /usage classification.
lifecycle events while preserving HTTP
429andSEMAPHORE_TIMEOUT.Provider-specific behavior
failures before SSE headers commit, replays normal thinking/content once, and closes readers and
iterators.
buildErrorBody(). Non-2xx TLS bodies are never reflected; network/TLS exceptions, upstream SSEerrors, and stream-reader failures become fresh locally constructed errors without upstream
causeor stack. Only integer status codes in the error range400..599survive projection;success/redirect, invalid, coercive, and hostile values fall back to
502.Proxies and sanitize transport metadata.
the route logs only the canonical sanitized message and imports the specific DB modules rather
than the restricted barrel.
Legal and provenance disposition
Primary sources:
fatihkabakk/tls-client-node@v0.2.0LICENSE — SHA-256086c687026ff693ad76589dda1af12304a3ff33fc5f15030035ed62ef6a6d6eb;fatihkabakk/tls-client-node@v0.2.0NOTICE — SHA-25680e5a526273788f2ace0164ec131daac697c54084add90855afdd03f5fadd3d3;bogdanfinn/tls-client@v1.15.1LICENSE — SHA-2567dab9a4dd66987fbe576d53c1ee047c193725df6f4fac67de315a127417fd151;bogdanfinn/tls-client@v1.15.1release — source of the six asset digests.The Commons Clause makes
tls-client-nodesource-available and commercially restrictive.Distributing notices does not grant a right to sell a product or service covered by that
restriction. The exception remains medium risk and temporary, owned by
@diegosouzapw, with reviewdue
2026-09-30/v3.9.0. Legal review, separate permission, or replacement with a permissivetransport is still required before commercial deployment. This PR records obligations and reduces
provenance risk; it is not legal advice and does not cure the restriction.
Current shared transport consumers:
chatgpt-web,claude-web,perplexity-web,grok-web,notion-web, andlmarena.TDD and focused verification
RED -> GREEN was captured for the native manifest/license/NOTICE contract, tampered binaries,
runtime version drift, unsafe install-lock replacement, lifecycle contention, hostile
instanceof/properties/coercion, credential-shapedError.name, Perplexity early quota and replay,public error/log sinks, provider validation fallback metadata, semaphore call-log persistence, the
CodeQL polynomial data-URL regex, omitted-media-type data URLs, LM Arena upstream stack/body
reflection, closed stream-transport projection, and rejection of public
2xx/3xxstatuses.Focused results on the final candidate:
53/55, then GREEN55/55124/124 PASS47/47 PASS54/54 PASS78/78 PASS121/121 PASS7/7 PASS7/7 PASS24/24 PASS41/41 PASS1/1 PASS85/85 PASSThe TLS parallel aggregate produced
46/47 LOAD-REDbecause the 100-caller stress exceeded its75-second limit under CPU contention; the same case passed alone in 48.3 seconds and in the serial
47/47matrix. A later broad ChatCore slice was4/11 INFRA-RED: seven cases were rejected by thereal resource-pressure guard while the cgroup was at 96.7%+ usage; the changed case passed alone.
Neither result is represented as a full-suite PASS.
Repository gates
PASS on exact candidate
29dc5427b6abd5a4ff5198759f8cfc2d500ce1aa:npm run typecheck:core;npm run check:cycles;git diff --check;npm run check:error-helper;npm run test:vitest -- --maxWorkers=1 --no-file-parallelism: the first run reached404/405because one
auto/glmcase timed out; that isolated slice then passed11/11, and the clean retrycompleted
405/405.PASS recorded earlier in the branch and unaffected by the final LM Arena-only commits:
node --check, 8/8);npm run check:file-size(4,408 production and 5,086 test files checked);npm run check:secrets -- --ratchet(0 findings);npm run check:any-budget:t11;npm run check:tracked-artifacts;npm run check:docs-allandnpm run check:docs-counts(only the two existing softexecutor-count notices);
npm run check:route-validation:t06;npm run check:build-scope.Exact-head full-suite and artifact evidence:
npm run test:vitest:ui -- --maxWorkers=1 --no-file-parallelismon29dc5427—285/285 files and
1,816/1,816tests in 1,417.66s, with an isolated temporaryDATA_DIR.Fast Production Build/npm run build:releaseon29dc5427in 18m21s;the
omniroute-buildartifact was uploaded (4,996,056,168 bytes).29dc5427.the classifier and are not counted as PASS.
29dc5427in 48m30s; Actions, C/C++, Python,and the Advanced Security aggregate also passed.
js/polynomial-redos), with no dismissal or suppression. Thefinal PR ref has no open code-scanning alert.
locateCommandignoreswhereoutput #968 (js/stack-trace-exposure), with no dismissal or suppression. Itsstored most-recent instance changed from
opentofixedafter the final analysis.Bounded CodeQL remediation history
b40a9fc3js/polynomial-redosand #968js/stack-trace-exposuree5d1bbc9d66748406f2xx/3xxvalues29dc5427400..599and rejects success/redirect, invalid, coercive, and hostile values53/55-> GREEN55/55; Arena matrix124/124; all CodeQL language jobs and the aggregate PASS; direct #967/#968 FIXEDIteration-1 remote evidence on
b40a9fc3includedFast Production BuildPASS in 18m56s, DASTPASS in 8m39s, Semgrep and Semgrep Cloud PASS, and all four CodeQL language jobs PASS. Iteration-2
remote evidence on
e5d1bbc9includedFast Production BuildPASS in 19m01s, DAST/Semgrep PASS,all CodeQL language jobs PASS, and the outer aggregator PASS. Quality Gates fast-path jobs skipped
by workflow classification are never counted as PASS.
Exact-base / non-candidate reds
npm run typecheck:noimplicit:corereports 36 errors only in unchangedopen-sse/translator/response/openai-responses/pureHelpers.ts; the file is byte-identical to theexact base, so this is
BASE-RED.docs/i18n/it/README.md,docs/i18n/tr/README.md, andtests/unit/grok-web.test.tsare Prettier-red on both the candidate and exact base; no broadreformat is included here.
npm run check:changelog-integritycompares this frozen.50tree with the newerorigin/release/v3.8.51and reports ten.51bullets as missing.CHANGELOG.mdon this candidateis byte-identical to exact base
091589...; only the owned changelog fragment is added, so this isbranch drift /
BASE-RED, not a candidate regression.koffifatal on the candidate and exact base:
BASE-RED.npm run check:circular-depsinvokes the shell shimnode_modules/.bin/dpdmthrough Node andcurrently fails before graph analysis; this is distinct from the passing
npm run check:cyclesresult above and is not called a candidate PASS.
npm run check:open-sse-typecheckreports a new-baseline mismatch only in unchangedsrc/app/api/v1/models/catalog.ts; the relevant source/config blobs are identical to the exactbase, so this remains
BASE-RED, not a candidate PASS.HOLD / not claimed
node_modulesis contaminated and cannot prove the exact dependency tree.
npm run check:pack-policy/ its realnpm pack --dry-rundid not finish within more than tenminutes and was interrupted without a verdict.
npm run test:unitand coverage.These HOLDs keep the PR in draft. They are not presented as PASS and do not authorize merge,
release, or deployment.
Known runtime boundaries
handle, inode/device, and nonce checks.
JavaScript TTL that can interrupt the native library.
200is committed cannot be promotedto an HTTP
429/502.Maintainer boundary