| Title | hiyouga LlamaFactory <= 0.9.5 Server-Side Request Forgery (CWE-918) |
|---|
| Description | ---
tags:
- bug-bounty
- llm
- ssrf
- python
- fastapi
alvo: hiyouga/LlamaFactory
servico: FastAPI /v1/chat/completions
cve: CVE pendente — bypass da correção de CVE-2025-61784
nivel: alto
status: confirmado-localmente-nao-reportado
data: 2026-09-20
commit: 97b32d3133b501432141a82949d5c7bc4d94f23a
---
# LlamaFactory — bypass de SSRF por redirecionamento HTTP
> [!warning]
> Pesquisa feita exclusivamente contra uma cópia local do código. Nenhuma instância pública do LlamaFactory foi testada. O achado ainda não foi enviado aos mantenedores.
Artefatos criados: [[Artefatos/poc_redirect_ssrf.py|PoC local seguro]].
## Como usar este documento
Este documento registra cronologicamente a análise do LlamaFactory, a tentativa que falhou por dependência ausente e a prova que confirmou o acesso a loopback. Ele também contém uma versão curta adequada para submissão privada ao mantenedor.
## Índice
- [[#1. Como pensar sobre a falha]]
- [[#2. Escopo e pré-requisitos]]
- [[#3. Seleção do snapshot]]
- [[#4. Busca inicial por sinks]]
- [[#5. Rastreamento da entrada até o request HTTP]]
- [[#6. Análise do filtro SSRF]]
- [[#7. Primeira tentativa e falha]]
- [[#8. Reprodução vencedora]]
- [[#9. Alcance pela API e autenticação]]
- [[#10. Impacto e severidade]]
- [[#11. Causa-raiz]]
- [[#12. Correção recomendada]]
- [[#13. Relatório para divulgação privada]]
- [[#14. Burp Suite]]
- [[#15. Troubleshooting]]
- [[#16. Cadeia resumida]]
- [[#17. Checklist]]
- [[#18. Glossário]]
## 1. Como pensar sobre a falha
SSRF ocorre quando um servidor aceita uma URL controlada pelo usuário e faz a requisição em nome dele. Isso é especialmente perigoso porque o servidor pode alcançar destinos invisíveis ao usuário externo, como `127.0.0.1`, painéis internos e serviços de metadata.
O filtro do LlamaFactory age como um porteiro que examina apenas o primeiro endereço de uma viagem. A URL inicial é pública, então recebe autorização. Entretanto, o servidor público responde com uma ordem de redirecionamento para um endereço interno. O cliente HTTP obedece à ordem sem consultar o porteiro novamente.
```text
Cliente
-> POST /v1/chat/completions com URL pública
-> check_ssrf_url(URL pública): aceita
-> requests.get(URL pública)
-> HTTP 302 Location: http://127.0.0.1:18765/ssrf-proof
-> requests segue o redirect automaticamente
-> serviço de loopback é alcançado
```
Pergunta ofensiva: **a validação acompanha cada salto da requisição ou somente o primeiro URL?**
Hipótese: como `requests.get()` segue redirects por padrão e o código não usa `allow_redirects=False`, um destino público pode redirecionar para rede privada e contornar `check_ssrf_url()`.
## 2. Escopo e pré-requisitos
### Escopo
- Repositório público: `https://github.com/hiyouga/LlamaFactory`
- Cópia local: `/root/LlamaFactory`
- Commit: `97b32d3133b501432141a82949d5c7bc4d94f23a`
- Teste interno: `127.0.0.1:18765`, controlado pelo pesquisador
- Redirecionador: `https://httpbin.org/redirect-to`
- Fora do escopo: instâncias de terceiros
### Ferramentas efetivamente utilizadas
| Ferramenta | Finalidade |
|---|---|
| `git` | Clonar e identificar exatamente o snapshot analisado |
| `grep` | Procurar sinks perigosos porque `rg` não estava instalado |
| `sed` e `nl` | Ler o código e preservar números de linha |
| Python 3 | Executar o validador real e hospedar o serviço controlado |
| `requests` | Reproduzir o mesmo comportamento HTTP usado pela aplicação |
Não foi necessário um IP de atacante remoto. O teste ocorreu inteiramente no host local, usando loopback.
## 3. Seleção do snapshot
### Hipótese/Objetivo
Fixar o código atual para que o resultado possa ser reproduzido mesmo depois de alterações no branch principal.
### O que fazer
```bash
git clone --depth 1 https://github.com/hiyouga/LlamaFactory.git /root/LlamaFactory
git -C /root/LlamaFactory rev-parse HEAD
```
### Como o comando foi montado
| Parte | Significado e motivo |
|---|---|
| `git clone` | Cria uma cópia local do repositório |
| `--depth 1` | Baixa apenas o snapshot mais recente, suficiente para a análise inicial |
| URL GitHub | Origem pública examinada |
| `/root/LlamaFactory` | Caminho explícito da cópia local |
| `git -C` | Executa o próximo subcomando dentro do diretório indicado |
| `rev-parse HEAD` | Mostra o hash exato do commit atual |
### Saída obtida
```text
Cloning into '/root/LlamaFactory'...
97b32d3133b501432141a82949d5c7bc4d94f23a
```
### O que observar
O hash torna a análise imutável e verificável.
### Decisão tomada
Usar esse commit como referência em todos os achados.
## 4. Busca inicial por sinks
### Hipótese/Objetivo
Aplicações LLM frequentemente processam modelos, mídia e arquivos externos. Procurar chamadas de rede, desserialização e subprocessos é um teste barato que reduz rapidamente a superfície.
### O que fazer
A primeira busca tentou usar `rg`, mas a ferramenta não estava instalada. A busca foi então repetida com `grep`:
```bash
grep -RInE --include='*.py' 'subprocess|Popen\(|os\.system|shell[[:space:]]*=[[:space:]]*True|torch\.load|pickle\.load|yaml\.(load|unsafe_load)|requests\.(get|post)|httpx\.|urlopen|urllib\.request|extractall|shutil\.unpack_archive' src scripts | head -n 300
```
### Como o comando foi montado
| Parte | Significado e motivo |
|---|---|
| `grep` | Pesquisa texto recursivamente |
| `-R` | Percorre subdiretórios |
| `-I` | Ignora arquivos binários |
| `-n` | Mostra número da linha |
| `-E` | Habilita expressão regular estendida |
| `--include='*.py'` | Limita a código Python |
| `\|` | Alternância da expressão regular, equivalente a “ou” |
| `src scripts` | Diretórios examinados |
| `|` | Envia a saída do `grep` ao próximo programa |
| `head -n 300` | Limita a visualização às primeiras 300 linhas |
### Saída obtida relevante
```text
src/llamafactory/api/chat.py:131: image_stream = requests.get(image_url, stream=True).raw
src/llamafactory/api/chat.py:144: video_stream = requests.get(video_url, stream=True).raw
src/llamafactory/api/chat.py:157: audio_stream = requests.get(audio_url, stream=True).raw
```
### O que observar
Três entradas multimodais diferentes chegam ao mesmo cliente HTTP. Isso amplia a alcançabilidade da falha.
### Decisão tomada
Rastrear a validação que precede essas três chamadas.
## 5. Rastreamento da entrada até o request HTTP
### Hipótese/Objetivo
Confirmar que a URL vem do corpo controlado pelo cliente e que existe um filtro imediatamente antes do sink.
### O que fazer
```bash
nl -ba src/llamafactory/api/chat.py | sed -n '115,165p'
```
### Como o comando foi montado
| Parte | Significado e motivo |
|---|---|
| `nl -ba` | Numera todas as linhas, inclusive linhas vazias |
| arquivo | Implementação do chat multimodal |
| `|` | Encaminha o texto numerado ao `sed` |
| `sed -n` | Não imprime automaticamente todas as linhas |
| `'115,165p'` | Imprime somente o intervalo relevante |
### Saída obtida relevante
```python
image_url = input_item.image_url.url
check_ssrf_url(image_url)
image_stream = requests.get(image_url, stream=True).raw
```
O mesmo padrão foi obtido para `video_url` e `audio_url`.
### O que observar
`input_item.*_url.url` vem da mensagem enviada à API. A função valida a string e depois `requests` inicia uma nova operação de resolução/conexão.
### Decisão tomada
Inspecionar se `check_ssrf_url()` vincula a validação à conexão e se revalida redirects.
## 6. Análise do filtro SSRF
### Hipótese/Objetivo
Verificar três classes comuns de bypass: múltiplos registros DNS, DNS rebinding e redirecionamentos HTTP.
### O que fazer
```bash
nl -ba src/llamafactory/api/common.py | sed -n '65,105p'
```
### Como o comando foi montado
Os parâmetros de `nl`, o pipe e o intervalo de `sed` têm o mesmo significado explicado na fase anterior. O in
Vendor coordination
Vendor contacted via GHSA-3w98-wx27-3fwr.
CVE assignment requested from VulDB; no CVE known at submission time. |
|---|
| Source | ⚠️ https://github.com/hiyouga/LlamaFactory/security/advisories/GHSA-3w98-wx27-3fwr |
|---|
| User | fernando viana (UID 101484) |
|---|
| Submission | 09/21/2026 02:15 (23 hours ago) |
|---|
| Moderation | 09/22/2026 00:04 (22 hours later) |
|---|
| Status | Duplicate |
|---|
| VulDB entry | 398842 [hiyouga LLaMA-Factory up to 0.9.5 OpenAI-compatible API Multimodal Media URL requests.get server-side request forgery] |
|---|
| Points | 0 |
|---|