Skip to content

Recusa caminho que sai do próprio pacote nos outros três ZIPs (issue #242) - #243

Merged
matbrgz merged 1 commit into
masterfrom
fix/242-zip-caminhos
Sep 16, 2026
Merged

matbrgz merged 1 commit into
masterfrom
fix/242-zip-caminhos

Conversation

@matbrgz

@matbrgz matbrgz commented Sep 16, 2026

Copy link
Copy Markdown
Member

Fecha #242.

Não é fuga de diretório, e a distinção importa

Medi antes de consertar. O extractTo() do PHP não deixa escapar do destino (PHP 8.5, libzip 1.22.8):

entrada no ZIP onde caiu
../a.txt alvo/a.txt
../../b.txt alvo/b.txt
x/../../c.txt alvo/c.txt
/tmp/abs-d.txt alvo/tmp/abs-d.txt

Nada saiu. Chamar isto de Zip Slip descreveria errado o que acontece, e um conserto guiado pela descrição errada mediria a coisa errada.

O que sobra

O PHP resolve a travessia reescrevendo o caminho em silêncio — sem erro, sem aviso.

x/../../data/secret/01.ans vira data/secret/01.ans e passa por cima de um arquivo legítimo do próprio pacote. Num pacote de problema isso troca um caso de teste: o pacote é aceito, o problema é importado, e o sintoma aparece longe da causa — uma submissão correta reprovada durante a prova, com o placar já andando.

SafeZipExtractorTest::test_php_flattens_instead_of_escaping_which_is_why_the_guard_exists guarda essa medição como teste, para que a razão não vire folclore. Se um dia ela falhar, a biblioteca mudou — e a guarda continua correta, porque não depende dela.

A guarda, e as quatro portas

SafeZipExtractor::escapingEntry() percorre os nomes antes de qualquer escrita e devolve o primeiro que for absoluto ou tiver um segmento ... extractTo() recusa o arquivo inteiro — não só a entrada ruim, porque um pacote que tenta reescrever caminhos não é um pacote do qual se queira aproveitar o resto, e meia extração no disco da máquina que serve a prova é pior que nenhuma.

porta recusa aparece como
backend.import-package frase na sessão, formulário preserva o que foi digitado
backend.import-boca/upload frase na sessão
POST /api/problems 422 com a mensagem
ProblemPackageService::importFromZip ZipPathException

O guardião local que o #239 tinha criado saiu, substituído pelo compartilhado.

Mutações

mutação resultado
tira o ramo do caminho absoluto ❌
tira o ramo do .. ❌
tira a guarda de dentro do extractTo ❌
BocaImporterService volta ao extractTo cru ❌
ProblemPackageService volta ao cru ❌
Api\ProblemController volta ao cru ❌

Controles positivos

Uma guarda larga demais passaria em todos os testes de recusa acima. Então: problem_statement/a..b.pdf, x/y..z/w.txt e ..oculto/x.txt são nomes legítimos e continuam passando, e um pacote bem formado continua sendo importado pela API com 201.

Verificação

  • ./vendor/bin/phpunit --no-coverage — 1608 testes, verde
  • PHPStan sem erros nos oito arquivos tocados
  • Pint: Api\ProblemController voltou a passar (eu tinha quebrado a ordem dos imports). BocaImporterService e ProblemPackageService já falhavam no master antes deste PR e ficaram como estão — reformatá-los inteiros esconderia esta mudança num diff de estilo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY

…242)

Não é fuga de diretório, e a distinção importa: medi o que o `extractTo()`
do PHP faz com `../a.txt`, `x/../../c.txt` e `/tmp/abs-d.txt`, e nenhum
escapa do destino. Um conserto guiado pela descrição errada mediria a coisa
errada.

O que sobra é pior de encontrar. O PHP resolve a travessia REESCREVENDO o
caminho em silêncio: `x/../../data/secret/01.ans` vira
`data/secret/01.ans` e passa por cima de um arquivo legítimo do próprio
pacote. Num pacote de problema isso troca um caso de teste — o pacote é
aceito, o problema é importado, e o sintoma aparece longe da causa: uma
submissão correta reprovada durante a prova, com o placar já andando.

`SafeZipExtractor` percorre os nomes antes de qualquer escrita e recusa o
arquivo INTEIRO se algum for absoluto ou tiver um segmento `..`. Inteiro, e
não só a entrada ruim, porque um pacote que tenta reescrever caminhos não é
um pacote do qual se queira aproveitar o resto — e meia extração no disco da
máquina que serve a prova é pior que nenhuma.

As quatro portas passam a usá-lo, cada uma devolvendo a recusa no formato
dela: frase na sessão no formulário de pacote ICPC e no do BOCA, 422 em
`POST /api/problems`, `ZipPathException` em `ProblemPackageService`. O
guardião local que o #239 tinha criado saiu, substituído pelo compartilhado.

A medição que motiva tudo isso virou teste
(`test_php_flattens_instead_of_escaping_which_is_why_the_guard_exists`) para
que a razão não vire folclore. Se um dia ela falhar, a biblioteca mudou — e a
guarda continua correta, porque não depende dela.

Seis mutações, cada uma derrubando pelo menos um teste: tirando o ramo do
caminho absoluto, tirando o do `..`, tirando a guarda do `extractTo`, e
voltando cada uma das três portas ao `extractTo` cru.

Controles positivos onde a guarda poderia ser larga demais: `a..b.pdf`,
`x/y..z/w.txt` e `..oculto/x.txt` são nomes legítimos e continuam passando,
e um pacote bem formado continua sendo importado pela API. Sem eles, uma
guarda que recusasse tudo passaria nos testes de recusa do mesmo jeito.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pvf2R8xtgbWQm1wRvjBqQY
@matbrgz
matbrgz merged commit f4f4f81 into master Sep 16, 2026
4 checks passed
@matbrgz
matbrgz deleted the fix/242-zip-caminhos branch September 16, 2026 13:56
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.

1 participant