Skip to content

Security scan results: response-header hardening and pinned CI actions #90

Closed

Description

@matbrgz

Rodei SAST, SCA e DAST no master em b8f081a. Nada explorável apareceu. Este issue registra o que foi rodado, o que foi triado como falso positivo e as duas coisas que sobraram como trabalho real — para que ninguém precise repetir a triagem.

Resultado por ferramenta

camada ferramenta resultado
SCA (PHP) composer audit limpo — "No security vulnerability advisories found"
SCA (JS) npm audit limpo — 0 vulnerabilidades em qualquer severidade
SAST semgrep (p/php, p/security-audit, p/owasp-top-ten) 19 achados, 0 confirmados após triagem
DAST OWASP ZAP baseline, app rodando com sqlite 0 FAIL, 14 WARN, 53 PASS

Os 53 PASS do ZAP importam tanto quanto os warnings: sem XSS refletido, sem SQLi, sem listagem de diretório, flags de cookie corretas, e POST /login sem token devolve 419 — CSRF está sendo aplicado de verdade.

Falsos positivos do semgrep (triados, não filar)

  • laravel-unsafe-validator em ProfileController.php:28 — a regra marca Rule::unique(...)->ignore($request->...) textualmente. O valor é $user->user_id, com $user = $request->user(): id do próprio autenticado, vindo da sessão, não de input.
  • tainted-filename em server.php:17 — é o server.php de fábrica do Laravel, e docker/nginx/default.conf:6 tem root /var/www/html/public. O arquivo não é alcançável por HTTP em produção. (Ver mesmo assim o último item abaixo.)
  • insecure-use-string-copy-fn ×5 em tools/safeexec.c:349-361 — todos são strncpy(dest, optarg, 255) seguidos de dest[255]=0 explícito, com BUFFSIZE 256. Corretos. (O safeexec sai de cena de qualquer forma — Judge sandbox: cap resident memory with cgroups #86.)
  • missing-user no Dockerfile — o comentário logo acima explica: o PHP-FPM sobe como root para ler config e abrir a porta, e cai para o usuário www definido no www.conf. É o desenho padrão e está documentado. O Dockerfile.judge tem USER judge.
  • request-host-used em docker/nginx/default.conf:74 — está dentro de um bloco comentado de HMR do Vite.

O que sobrou: cabeçalhos de resposta

docker/nginx/default.conf já manda X-Frame-Options, X-Content-Type-Options e Referrer-Policy. O ZAP apontou os que faltam:

  • Content-Security-Policy — ausente em todas as páginas. É a mitigação de XSS que sobra depois do escaping do Blade, e a aplicação serve conteúdo controlado por usuário (enunciados, nomes de equipe, saída de compilador em submission-show)
  • Permissions-Policy — ausente
  • Cross-Origin-Embedder-Policy / Cross-Origin-Opener-Policy / Cross-Origin-Resource-Policy — ausentes
  • Strict-Transport-Security — ausente. O bloco TLS do nginx está comentado; quando for habilitado, o HSTS vai junto

Ignorar: "Big Redirect", "Non-Storable Content", "Session Management Response Identified", "Modern Web Application" e "Authentication Request Identified" são informativos do ZAP, não defeitos.

O que sobrou: CI

  • Pinar as actions em SHA de commit. O .github/workflows/ci.yml usa actions/checkout@v4, shivammathur/setup-php@v2, docker/build-push-action@v6 etc. Tag é mutável: quem controla o repositório da action pode movê-la e passar a executar código diferente num workflow que tem acesso ao GITHUB_TOKEN e ao cache. 10 ocorrências, todas minhas — o semgrep pegou código que eu mesmo escrevi ontem.

Limpeza, de baixo risco

  • Remover resources/views/welcome.blade.php e server.php. São restos do esqueleto do Laravel: o welcome carrega uma folha de estilo do Google Fonts sem SRI (o único recurso externo da aplicação, e a origem do warning de SRI do ZAP) e linka documentação do Laravel. O server.php não é usado desde o Laravel 11.

Como reproduzir

docker run --rm -v "$PWD":/app -w /app composer:latest composer audit
npm audit
docker run --rm -v "$PWD":/src -w /src semgrep/semgrep semgrep \
  --config=p/php --config=p/security-audit --config=p/owasp-top-ten \
  --exclude=vendor --exclude=node_modules --exclude=boca-master
# DAST: subir a app com sqlite e apontar o ZAP
docker run --rm --network <rede> ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t http://<app>:8000

Nota para quem for repetir o DAST: php artisan serve não repassa variáveis de ambiente arbitrárias ao processo filho (só a allowlist do ServeCommand), então -e SESSION_DRIVER=file é ignorado e a app tenta o Redis do .env. Usar php -S 0.0.0.0:8000 -t public server.php.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions