---
id: owasp-secure-coding-gates
domain: security
agents: [qa]
when: "ao revisar código quanto a segurança antes de commit/merge"
---

# OWASP Secure Coding Gates — o que reprova ou aprova um diff

> Fonte: OWASP CheatSheetSeries (github.com/OWASP/CheatSheetSeries) — XSS Prevention,
> SQL Injection Prevention, Password Storage, Authentication, Session Management,
> Input Validation e CI/CD Security Cheat Sheets. As regras abaixo são citação/derivação direta
> dessas folhas, não opinião.

## O problema

Code review de segurança vira teatro quando o revisor escreve "valide o input" e "cuidado com XSS"
e aprova. Isso não é gate — é prosa. Um gate de verdade tem **critério binário**: olha-se o sink, o
contexto e o construtor da query, e decide-se REPROVA ou APROVA com base num fato verificável no
diff. Os tells de revisão fraca:

- "Sanitize tudo" — sanitização global no input é a **defesa errada**; o certo é encoding por
  **contexto de saída** e parametrização na query. Quem sanitiza no input deixa passar o que importa.
- Aceitar `escape()` manual de aspas em SQL como mitigação ("STRONGLY DISCOURAGED" pela OWASP).
- Aprovar hash de senha com SHA-256/MD5 "com salt" achando que resolve.
- Pinar GitHub Action por tag (`@v4`) achando que é versão fixa — tag é mutável.
- Confiar em validação client-side de input.

## O conhecimento / os princípios

### 1. XSS — encoding é por CONTEXTO de saída, não global

A regra-mãe da OWASP: a mesma variável precisa de encoding **diferente** dependendo de **onde** ela é
escrita no HTML. Não existe "encoding universal". Cada contexto tem seu sink seguro e seu esquema.

| Contexto de saída | Encoding exigido | Sink seguro (aprova) | Sink perigoso (reprova) |
|---|---|---|---|
| Corpo HTML (`<div>$var</div>`) | HTML entity: `&`→`&amp;` `<`→`&lt;` `>`→`&gt;` `"`→`&quot;` `'`→`&#x27;` | `.textContent`, `createTextNode()`, `.insertAdjacentText()` | `innerHTML`, `outerHTML`, `document.write()` |
| Atributo HTML (`<div id="$var">`) | HTML attribute encoding `&#xHH;` **+ aspas obrigatórias** em volta do valor | `.setAttribute(nomeFixo, var)`, `.className` | atributo sem aspas; nome de atributo dinâmico |
| JavaScript (dentro de `<script>` ou `on*`) | JS encoding `\xHH`, **só em valor entre aspas** | `JSON.parse`/dados, nunca código | `eval()`, `setTimeout(str)`, `setInterval(str)`, gerar código com a var |
| URL (`href`/`src`) | URL encoding `%HH`, **depois** HTML attribute encoding | `encodeURIComponent(x)` | concatenar em `href` sem validar esquema (`javascript:` passa) |
| CSS (valor de propriedade) | só em **valor** de propriedade CSS | `style.property = x` | var em nome de propriedade ou seletor; `url(javascript:...)` |

**Contextos perigosos — REPROVA sempre que a var entra direto neles** (encoding não protege):
`<script>` diretamente, dentro de comentário HTML `<!-- -->`, `<style>` direto, nome de handler de
evento, callback de função.

**HTML autorado pelo usuário** (precisa permitir tags): único caminho aprovado é sanitização com
**DOMPurify** — `let clean = DOMPurify.sanitize(dirty);` — e **nunca** modificar a string depois de
sanitizada (re-mutação reabre o buraco).

Framework com auto-escape (React/Angular/Lit) **aprova por padrão**, mas REPROVA se o diff usa o
escape-hatch sem sanitização: `dangerouslySetInnerHTML` (React), `bypassSecurityTrustAs*` (Angular),
`unsafeHTML` (Lit), `inner-h-t-m-l` (Polymer).

### 2. SQL Injection — parametrização, não escaping

A defesa primária é **prepared statement com query parametrizada**: o SQL é definido inteiro primeiro,
e o valor entra como bind variable depois. Concatenar input na string da query é REPROVA imediata.

```java
// REPROVA — concatenação de input na query
String query = "SELECT account_balance FROM user_data WHERE user_name = "
             + request.getParameter("customerName");
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery(query);

// APROVA — prepared statement, valor via setString
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, request.getParameter("customerName"));
ResultSet rs = pstmt.executeQuery();
```

```java
// ORM também: HQL concatenado REPROVA
Query bad  = session.createQuery("from Inventory where productID='" + userParam + "'");
// APROVA — named parameter
Query good = session.createQuery("from Inventory where productID=:productid");
good.setParameter("productid", userParam);
```

`escape()` manual de aspas é **"STRONGLY DISCOURAGED"** e "fragile compared to other defenses" — não
aceitar como mitigação suficiente.

**O que NÃO pode ser parametrizado** (bind variable não cobre): **nome de tabela, nome de coluna,
e ordem `ASC`/`DESC`**. Nesses casos a única defesa aprovada é **allow-list mapeando para valores
fixos**, nunca passar o input adiante:

```java
// APROVA — nome de tabela via allow-list (switch), nunca concatenado
switch (param) {
  case "Value1": tableName = "fooTable"; break;
  case "Value2": tableName = "barTable"; break;
  default: throw new InputValidationException("unexpected value provided for table name");
}
// APROVA — sort order mapeado a literal, não concatenado do input
String sql = "... order by Salary " + (sortOrder ? "ASC" : "DESC");
```

### 3. Armazenamento de senha — ordem de algoritmo e parâmetros mínimos

Hash genérico (MD5, SHA-1, SHA-256), mesmo com salt, **REPROVA**. A OWASP define ordem de preferência
e parâmetros numéricos mínimos:

| Ordem | Algoritmo | Parâmetros mínimos (OWASP) |
|---|---|---|
| 1º | **Argon2id** | `m=19456` (19 MiB), `t=2`, `p=1` (ou m=47104/t=1, ou m=7168/t=5) |
| 2º | **scrypt** (se Argon2id indisponível) | `N=2^17` (128 MiB), `r=8`, `p=1` |
| 3º | **bcrypt** (legado) | work factor **≥ 10**; limite de **72 bytes** de senha |
| 4º | **PBKDF2** (só p/ FIPS-140) | SHA-256: **600.000** iter · SHA-512: **220.000** iter |

bcrypt tem dois caveats que REPROVAM se ignorados: o limite de 72 bytes e o **null byte** truncando o
hash. Pré-hash recomendado: `bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost)`.

**Pepper** (opcional, reforça): é compartilhado entre as senhas, **não** é armazenado junto do hash —
fica em secrets vault/HSM, separado do banco.

### 4. Autenticação — lockout, mensagens genéricas, comprimento, MFA

| Regra | Critério (REPROVA se violado) |
|---|---|
| Anti-brute-force | Lockout por **conta** (não por IP). Threshold + janela de observação + duração; ideal **exponencial** (1s dobrando a cada falha). CAPTCHA/throttling complementam. |
| Mensagem de erro | **Genérica e idêntica** p/ usuário inexistente, senha errada ou conta desabilitada: `"Login failed; Invalid user ID or password."` Revelar qual falhou = enumeração = REPROVA. |
| Recuperação de senha | Mensagem genérica: `"If that email address is in our database, we will send you an email to reset your password"`. |
| Comprimento mínimo | **8** com MFA ativo · **15** sem MFA. Máximo **≥ 64** (permitir passphrase). Aceitar Unicode/espaço; sem regra de composição obrigatória. |
| MFA | "by far the best defense" — teria prevenido **99.9%** dos comprometimentos. Exigir em fluxos sensíveis. |

### 5. Gestão de sessão — atributos de cookie e ciclo de vida

Cookie de sessão tem que carregar **todos** estes atributos — falta de qualquer um REPROVA:

```
Set-Cookie: __Host-SessionID=<value>; Secure; HttpOnly; SameSite=Strict; Path=/
```

| Atributo | Exigência |
|---|---|
| `Secure` | obrigatório — só trafega via HTTPS |
| `HttpOnly` | obrigatório — JS não lê via `document.cookie` (corta roubo por XSS) |
| `SameSite` | `Strict` (preferido) ou `Lax`. **Nunca** `None` sem `Secure`. |
| Prefixo `__Host-` | recomendado p/ session ID (força Secure + Path=/ + sem Domain) |

| Propriedade | Critério |
|---|---|
| Entropia do session ID | **≥ 64 bits** (≥ 16 chars hex); gerar via **CSPRNG** com saída ≥ 128 bits. Conteúdo "meaningless" — sem PII. |
| Idle timeout | 2–5 min (alto valor) · 15–30 min (baixo risco) |
| Absolute timeout | 4–8 h |
| Regeneração | **Regenerar o ID após login / qualquer mudança de privilégio** (`session_regenerate_id(true)` em PHP). Não regenerar = fixação de sessão = REPROVA. |
| Logout | Invalidar server-side (framework) **e** expirar o cookie no cliente (`Expires` no passado / `Max-Age=0`); `Cache-Control: no-store`. |

### 6. Validação de input — allow-list, server-side, e não é defesa primária

| Princípio | Regra |
|---|---|
| Allow-list > deny-list | Defina **o que É permitido**; tudo o mais é negado. Deny-list "é trivial de burlar" e bloqueia input válido (`O'Brian`). Deny-list só como camada extra, nunca primária. |
| Onde validar | **Server-side, antes de processar.** Validação client-side (JS) "can be circumvented" — não conta como controle. |
| Syntactic vs semantic | Syntactic: formato (SSN, data, moeda). Semantic: regra de negócio (start < end, preço na faixa). |
| Escopo | Validação **não** é defesa primária contra XSS/SQLi — "should not be used as the primary method". Reduz impacto, mas **encoding por contexto e parametrização continuam sendo a defesa primária**. Aprovar input validation no lugar de parametrização = REPROVA. |

### 7. Segurança de CI/CD — secrets, least-privilege, pin por SHA

| Área | Critério (REPROVA se violado) |
|---|---|
| Secrets no código | **Nunca** hardcoded em repo ou config de pipeline. Detectar com **git-leaks/git-secrets**. Não imprimir/logar em console, log ou histórico de comando. |
| Vault + credenciais curtas | Usar HashiCorp Vault / AWS Secrets Manager / CyberArk. Preferir **credenciais temporárias/OTP** (ou OIDC) a token longevo. |
| Least privilege | "deny by default". Pipeline com mínimo de permissão; conta de OS do runner **sem root**. Não compartilhar credencial entre pipelines de sensibilidades diferentes. |
| Pin de dependência/action | **Version pinning + verificação de hash/checksum** contra hash bom conhecido. Em lockfile (`package-lock.json`/`Pipfile.lock`) e **enforce o lockfile**. Para GitHub Actions, pinar por **commit SHA** (tag é mutável → REPROVA pinar por `@v4`). |
| SCM | Branches protegidos; PR **revisado e não-bypassável** antes do merge; **sem auto-merge**; MFA habilitado; commits **assinados** (Sigstore/Signserver). |
| Execução | Builds em nós isolados; comunicação SCM↔CI via **TLS 1.2+**; restringir acesso por IP; **aprovação manual** antes de deploy em produção; Docker **sem `--privileged`**. |
| Logs | Formato parseável (JSON/syslog); **nunca** logar senha/token/API key em texto puro; centralizar em SIEM com alertas. |

## Checklist (responda — qualquer "não" reprova o diff)

XSS
- [ ] Toda var dinâmica usa encoding do **contexto** correto (HTML body / attr / JS / URL / CSS)?
- [ ] Nenhum `innerHTML`/`document.write`/`eval`/`setTimeout(string)` com dado de usuário?
- [ ] HTML autorado pelo usuário passa por **DOMPurify** e não é mutado depois?
- [ ] Nenhum escape-hatch de framework (`dangerouslySetInnerHTML`, `bypassSecurityTrustAs*`) sem sanitização?

SQLi
- [ ] Toda query usa **prepared statement / bind variable** (zero concatenação de input)?
- [ ] Nome de tabela/coluna/`ASC|DESC` vem de **allow-list**, não do input?
- [ ] Nenhuma mitigação baseada só em `escape()` de aspas?

Auth & senha
- [ ] Senha hasheada com **Argon2id/scrypt/bcrypt** nos parâmetros mínimos (não SHA/MD5)?
- [ ] Lockout por conta + mensagem de erro genérica (sem enumeração)?
- [ ] Mínimo 8 (c/ MFA) ou 15 (s/ MFA), máximo ≥ 64?

Sessão
- [ ] Cookie com `Secure; HttpOnly; SameSite=Strict/Lax` (e `__Host-` no session ID)?
- [ ] ID com ≥ 128 bits de CSPRNG; **regenerado no login**; invalidado no logout?
- [ ] Idle e absolute timeout definidos?

Input & CI/CD
- [ ] Validação **allow-list, server-side** — e não substituindo parametrização/encoding?
- [ ] Zero secret hardcoded; vault + credencial curta/OIDC?
- [ ] Actions/deps pinadas por **SHA + hash** em lockfile enforçado; runner sem root, sem `--privileged`?
- [ ] PR revisado não-bypassável, sem auto-merge, aprovação manual p/ produção?

## Tabela de decisão "use X quando Y"

| Quando (Y) | Use (X) |
|---|---|
| Var vai dentro de `<div>`/corpo HTML | HTML entity encoding ou `.textContent` |
| Var vai num atributo HTML (`id`, `class`...) | attribute encoding `&#xHH;` + aspas + `setAttribute(nomeFixo, var)` |
| Var vai dentro de `<script>` / handler `on*` | JS encoding `\xHH` só em valor entre aspas; nunca gerar código |
| Var vira `href`/`src` | `encodeURIComponent` + attribute encoding; validar esquema (bloquear `javascript:`) |
| Usuário precisa enviar HTML rico | DOMPurify, sem mutar depois |
| Valor de usuário num `WHERE`/`VALUES` | prepared statement + bind variable |
| Parte da query é nome de tabela/coluna/sort | allow-list → literal fixo (switch/map) |
| Armazenar senha nova | Argon2id (m=19456,t=2,p=1); senha longa → pré-hash bcrypt c/ HMAC |
| Compliance FIPS-140 impede Argon2 | PBKDF2-HMAC-SHA256 600k iter |
| Cookie de sessão | `__Host-`/`Secure; HttpOnly; SameSite=Strict; Path=/` |
| Após autenticar / elevar privilégio | regenerar session ID |
| Filtrar input de usuário | allow-list, server-side (complemento, não substituto de encoding/parametrização) |
| Credencial no pipeline | vault + token curto/OIDC, nunca hardcoded |
| Consumir GitHub Action / dependência externa | pin por commit SHA + hash em lockfile enforçado |
| Pipeline acessa recurso de plataforma | least privilege, deny-by-default, runner sem root |
