From fe731e83c664edb8a85d46fce553c1d76e7449ac Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Mon, 23 Feb 2026 21:00:01 -0600 Subject: [PATCH] feat: implement rotating background hero on companies page --- docs/APPSEC_STRATEGY.md | 68 +++++++++++++++++++++++++++++ frontend/src/app/companies/page.tsx | 51 ++++++++++++++++------ 2 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 docs/APPSEC_STRATEGY.md diff --git a/docs/APPSEC_STRATEGY.md b/docs/APPSEC_STRATEGY.md new file mode 100644 index 0000000..53e4593 --- /dev/null +++ b/docs/APPSEC_STRATEGY.md @@ -0,0 +1,68 @@ +# Application Security (AppSec) Estratégia e Plano de Mitigação: GoHorseJobs + +Atuando como seu Especialista em AppSec para o ecossistema React e Next.js do GoHorseJobs, elaborei este plano de testes de segurança, validação de vulnerabilidades e mitigação, com base nas características comuns da nossa stack. + +## 1. Análise da Arquitetura e Mitigação + +### 1.1. Arquitetura de Autenticação e Sessão +* **Contexto Atual (GoHorseJobs):** O projeto utiliza JWT customizado com o backend (Fastify/Node.js). No frontend (Next.js), vimos o uso de `localStorage` para armazenar tokens (ex: no `api.ts`), embora possa haver uso de NextAuth para provedores em algumas ramificações. +* **Risco Técnico:** Armazenar JWTs sensíveis no `localStorage` os expõe a ataques de XSS (Cross-Site Scripting). Qualquer script malicioso injetado pode ler o `localStorage`. +* **Plano de Mitigação:** + * **Migração para HttpOnly Cookies:** O backend deve enviar o JWT em um cookie `HttpOnly`, `Secure` e `SameSite=Strict` ou `Lax`. O frontend Next.js automaticamente enviará este cookie nas requisições, sem que o código JS precise ter acesso a ele. + * **Middleware do Next.js:** Continuar ou implementar a proteção de rotas privadas (ex: `/dashboard/*`) no arquivo `middleware.ts` do Next.js. Isso evita renderização de páginas privadas antes de verificar a autenticação, protegendo contra vazamento de UI. + +### 1.2. Comunicação e Dados +* **Server Actions & Validação:** + * **Contexto:** O projeto Next.js atual foca muito em Client Components buscando de rotas Next API ou direto do Backend. Se Server Actions forem introduzidas, elas são endpoints equivalentes. + * **Mitigação:** **NUNCA** confie no input do cliente. Mesmo em Server Actions, use validação rígida de *schemas* (ex.: biblioteca **Zod** ou Yup). Validar tipos, tamanhos e formatos antes de processar. +* **Variáveis de Ambiente:** + * **Regra de Ouro:** Variáveis com prefixo `NEXT_PUBLIC_` são injetadas no bundle JavaScript do navegador. **NUNCA** coloque chaves secretas (API keys privadas, senhas de DB) em variáveis `NEXT_PUBLIC_`. Elas devem ser exclusivas do servidor. +* **Sanitização & CSP:** + * **CSP (Content Security Policy):** A implementação de cabeçalhos CSP via `next.config.js` é crucial. Ela restringe de onde os scripts, imagens e estilos podem ser carregados, mitigando drasticamente ataques de XSS e injeção de dados. + +### 1.3. Pontos de Exposição +* **APIs Externas:** Chaves do Google Maps, Stripe Public Key, etc., são normais no cliente, mas devem possuir restrições de domínio configuradas nos respectivos provedores para evitar abuso caso copiadas. +* **Injeção de Conteúdo:** O Next.js/React sanitiza variáveis no JSX por padrão (ex: `
{userInput}
` é seguro). O perigo mora no `dangerouslySetInnerHTML`. + * **Regra:** Se precisar usar `dangerouslySetInnerHTML` para renderizar Rich Text (ex: descrições longas de vagas), passe o conteúdo por uma biblioteca de sanitização isomórfica como o **DOMPurify** antes. + +--- + +## 2. Checklist de "Security Smells" (Maus Cheiros) em Next.js + +Revise a codebase do GoHorseJobs buscando estes padrões perigosos: + +- [ ] **Smell 1: "The Local Storage Hoarder"**: Tokens de acesso, PII (Personal Identifiable Information) ou dados sensíveis guardados no `localStorage` ou `sessionStorage`. +- [ ] **Smell 2: "The Naked Server Action"**: Server Actions (funções com `'use server'`) que não realizam verificação de autorização (ex: verificar se `user.role === 'ADMIN'`) ou não validam os inputs com Zod, assumindo que só o frontend vai chamá-las. +- [ ] **Smell 3: "The Trusting Inner HTML"**: Uso de `dangerouslySetInnerHTML={{ __html: userProvidedData }}` sem sanitização prévia com DOMPurify. +- [ ] **Smell 4: "The Leaky Config"**: Chaves sensíveis como `DATABASE_URL` ou `STRIPE_SECRET_KEY` configuradas (por engano) com o prefixo `NEXT_PUBLIC_`. +- [ ] **Smell 5: "The Unrestricted CORS"**: O backend (GoHorse API) permitindo `Access-Control-Allow-Origin: *` em rotas com dados sensíveis, em vez de restringir aos domínios da Vercel/Coolify. +- [ ] **Smell 6: "The Open Redirect"**: Endpoints ou páginas (como `/login?callbackUrl=/malicious-site.com`) que redirecionam baseados em query parameters não validados. + +--- + +## 3. Testando Vulnerabilidades: IDOR e XSS em Next.js + +### Mapeando IDOR (Insecure Direct Object Reference) +IDOR ocorre quando um usuário consegue acessar ou modificar dados de outro usuário apenas alterando um ID na requisição. +* **No Contexto (Ex. Edição de Vaga):** + * **Como testar:** Faça login como `Empresa A`. Tente acessar ou enviar uma mutação (PATCH/DELETE) para a API `/api/jobs/123`, onde `123` é o ID de uma vaga da `Empresa B`. + * **Server Components/Actions:** Se o componente busca `await getJob(params.id)`, o BD precisa verificar *no lado do servidor* se o usuário autenticado (`session.user.id`) é o dono (`companyId === session.user.companyId`) daquele `id`. + +### Mapeando XSS (Cross-Site Scripting) +XSS focado em Stored XSS e Reflected XSS. +* **No Contexto (Ex. Criação de Vaga ou Ticket):** + * **Como testar:** Ao submeter um formulário de Vaga ou Contato, insira na "Descrição" ou "Título" os payloads clássicos: ``, ``, ou `javascript:require('child_process')` (foco em quebrar algo se renderizado no server render). + * **Como validar:** Navegue até a página que exibe essa vaga. Se o alerta estourar ou a imagem quebrada injetar código, o XSS é válido. O React previne isso em texto, mas falha se o campo alimentar um atributo `dangerouslySetInnerHTML` desprotegido ou um href dinâmico em tags ``. + +--- + +## 4. Integração de Ferramentas de Teste + +Para garantir a segurança contínua no pipeline do GoHorseJobs: + +1. **SAST (Static Application Security Testing) com Snyk:** + * **Integração:** Conecte o repositório do Github ao Snyk. Ele escaneia continuamente o `package.json` em busca de bibliotecas npm vulneráveis e analisa o código React/Next procurando *Security Smells* (hardcoded secrets, injeções óbvias). +2. **DAST (Dynamic Application Security Testing) com OWASP ZAP:** + * **Integração:** ZAP pode ser executado contra um ambiente de *staging* do Next.js via Github Actions ou Coolify pipelines. Ele ataca a API rodando interceptando tráfego e testando injeções ativas, ajudando a pegar falhas de CORS e cabeçalhos faltando. +3. **Testes de Contrato para Prevenção de Vazamento (Mass Assignment):** + * Use **Zod** tanto para validar a *entrada* quanto para a *saída* da API/Server Action. Ex: Se o frontend pede `/users/1`, a API deve retornar apenas `{ id, name, email }`. Se retornar acidentalmente `{ ...user, passwordHash: '...', resetToken: '...' }`, ocorreu vazamento de dados. O Zod de contrato (`UserResponseSchema.parse(dbUser)`) corta fora o que não está no contrato antes de enviar ao cliente. diff --git a/frontend/src/app/companies/page.tsx b/frontend/src/app/companies/page.tsx index ba6c39e..9c36c9c 100644 --- a/frontend/src/app/companies/page.tsx +++ b/frontend/src/app/companies/page.tsx @@ -27,6 +27,22 @@ export default function CompaniesPage() { const [searchTerm, setSearchTerm] = useState("") const [selectedIndustry, setSelectedIndustry] = useState("Todas") const [selectedSize, setSelectedSize] = useState("Todos") + const [currentImageIndex, setCurrentImageIndex] = useState(0) + + const heroImages = [ + "sp.png", + "rj.png", + "tokyo.png", + "ba.png", + "london.png" + ] + + useEffect(() => { + const timer = setInterval(() => { + setCurrentImageIndex((prev) => (prev + 1) % heroImages.length) + }, 5000) + return () => clearInterval(timer) + }, []) const industries = [ "Todas", @@ -157,7 +173,7 @@ export default function CompaniesPage() { const filteredCompanies = companies.filter(company => { const matchesSearch = company.name.toLowerCase().includes(searchTerm.toLowerCase()) || - company.description.toLowerCase().includes(searchTerm.toLowerCase()) + company.description.toLowerCase().includes(searchTerm.toLowerCase()) const matchesIndustry = selectedIndustry === "Todas" || company.industry === selectedIndustry const matchesSize = selectedSize === "Todos" || company.employees === selectedSize @@ -172,18 +188,27 @@ export default function CompaniesPage() {
{/* Hero Section */} -
-
- Empresas -
-
+
+ + + Financial Center + + +