feat: implement rotating background hero on companies page
This commit is contained in:
parent
1b897eeb8e
commit
fe731e83c6
2 changed files with 106 additions and 13 deletions
68
docs/APPSEC_STRATEGY.md
Normal file
68
docs/APPSEC_STRATEGY.md
Normal file
|
|
@ -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: `<div>{userInput}</div>` é 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: `<script>alert('XSS')</script>`, `<img src="/" onerror="alert(document.cookie)">`, 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 `<a>`.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
|
@ -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() {
|
|||
|
||||
<main className="flex-1">
|
||||
{/* Hero Section */}
|
||||
<section className="relative py-20 md:py-28 overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/empresas.jpg"
|
||||
alt="Empresas"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
quality={100}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 z-10 bg-black opacity-20"></div>
|
||||
<section className="relative py-20 md:py-28 overflow-hidden h-[60vh] min-h-[500px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentImageIndex}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1.5 }}
|
||||
className="absolute inset-0 z-0"
|
||||
>
|
||||
<Image
|
||||
src={`/cities/${heroImages[currentImageIndex]}`}
|
||||
alt="Financial Center"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
quality={100}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<div className="absolute inset-0 z-10 bg-black/40 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
|
||||
<div className="absolute inset-0 opacity-10 z-20">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_30%_50%,rgba(255,255,255,0.2)_0%,transparent_50%)]"></div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue