Dia 6 — Segurança do servidor
CORS, cabeçalhos e limites de taxa
Os três guardas da mesma porta
O helmet e o express-rate-limit não estão instalados no material, então o exemplo reproduz o que eles fazem: cabeçalhos na resposta, um balde de fichas por IP, e a decisão de origem. O comportamento é o que importa, e ele é medido.
A ordem deles é a ordem da ameaça:
| Guarda | Pergunta que responde |
|---|---|
| CORS | o navegador pode mostrar esta resposta na tela? |
| cabeçalho de segurança | o que o navegador faz com o que recebeu? |
| rate limit | quantas vezes este cliente pode chamar? |
Nenhum dos três impede a requisição de chegar no servidor. CORS não é firewall, cabeçalho não é autenticação, e rate limit não é validação de senha.
CORS é sobre a leitura da resposta
CORS responde a uma pergunta única: o navegador pode entregar essa resposta para o código da página? Quem aplica a regra é o navegador, não o servidor — e é por isso que o servidor não pode "negar": ele apenas não entrega o header.
O exemplo faz a mesma chamada duas vezes, mudando só o Origin:
POST com origem permitida -> status 200, Access-Control-Allow-Origin: https://app.exemplo.com POST com origem nao permit. -> status 401, Access-Control-Allow-Origin: (ausente) mesma rota, mesma senha errada, mesmo status: o servidor se comportou igual a UNICA diferenca e o header ausente
As duas requisições produziram o mesmo status — o login falhou pelo mesmo motivo nas duas. O que muda é o header: a segunda resposta não traz Access-Control-Allow-Origin, e o navegador entrega a resposta a um script que pode ler qualquer coisa.
O caminho do header é uma lista explícita:
const ORIGENS_PERMITIDAS = [ 'https://app.exemplo.com', 'http://localhost:8080', ];
Access-Control-Allow-Origin: * vale para qualquer site. É o que a documentação sugeriu durante anos e o que transformou "API pública" em "API de qualquer página". O Vary: Origin é o detalhe que impede o cache de misturar as duas respostas — a resposta muda conforme a origem, e o cache precisa saber disso.
O preflight é a pergunta que o navegador faz antes do POST:
preflight OPTIONS -> status 204 (o navegador pergunta antes de mandar o POST) Access-Control-Max-Age: 600 — o navegador guarda a resposta do preflight por 600s
É um OPTIONS que não chega a tocar o banco. Quem responde 204 sem consulta é a diferença entre um servidor que aguenta 3 requisições e um que aguenta 6.
Cabeçalhos de segurança: instruções para o navegador
Content-Security-Policy default-src 'self'; script-src 'self'; object-src 'none' X-Content-Type-Options nosniff X-Frame-Options DENY Referrer-Policy no-referrer Strict-Transport-Security max-age=31536000; includeSubDomains X-Powered-By (vazio, remove o header)
Cada um diz uma coisa:
| Cabeçalho | Instrução |
|---|---|
Content-Security-Policy | de onde o script pode vir |
X-Content-Type-Options: nosniff | não adivinhar o tipo do arquivo |
X-Frame-Options: DENY | esta página não vai dentro de <iframe> |
Referrer-Policy | o que vaza na URL quando o usuário clica em link |
Strict-Transport-Security | só volta por HTTPS, por um ano |
X-Powered-By vazio | não declarar a tecnologia usada |
X-Powered-By: Express é uma dica de graça para quem procura por \x3csomething\x3e,versão vulnerável. Vazio remove a informação.
helmet é uma lista pronta desses cabeçalhos — usar a biblioteca é melhor do que escrever a lista, porque a lista cresce e ninguém lembra de acrescentar o cabeçalho novo.
Rate limit: o balde de fichas
A ameaça que o limite de requisições existe para deter é o brute force: alguém repetindo login, senha por senha, sem saber a senha e sem querer saber quem é. A mesma forma aparece em outro alvo — alguém disparando requisição atrás de requisição até o servidor não dar conta. Isso é negação de serviço, e as duas são a mesma entrada pelo mesmo portão.
Limitar por IP é a forma mais comum de conter as duas, e a justificativa é operacional: o atacante tem uma origem, o balde é por origem, e a rajada acaba. Requisição por minuto é a unidade que se lê de cabeça — cinco por minuto no login, algumas centenas na listagem — e express-rate-limit aceita esse número direto.
O limite mais comum não é "não pode passar de N por minuto". É um balde com N fichas, e uma ficha que volta a cada segundo.
function criarLimitador({ capacidade, repousoMs }) { const baldes = new Map(); // ip -> { fichas, ultimoRepouso } // ... }
O Map por IP é o que separa um usuário de outro: sem ele, o limite é global e uma pessoa atrás do mesmo proxy de escritório derruba o acesso de todo mundo.
O exemplo mede a transição real:
tentativa 1 -> 401 restantes=2 tentativa 2 -> 401 restantes=1 tentativa 3 -> 401 restantes=0 tentativa 4 -> 429 restantes=(sem header) Retry-After=1s tentativa 5 -> 429 restantes=(sem header) Retry-After=1s
Três requisições chegaram ao login; da quarta em diante, 429 Too Many Requests. O contador vai 2, 1, 0 — e só depois do 429. O Retry-After diz quando tentar de novo, em segundos.
Duas decisões que o exemplo deixa explícitas:
O limite vem antes do trabalho. permite(ip) roda antes de qualquer consulta. Limitar depois de gastar MySQL deixa o ataque pagar o preço completo antes de ser barrado — o limite protege o banco, e para proteger o banco ele tem que vir antes.
O 429 não grava nada. Depois das cinco tentativas:
sessoes criadas no banco: 1 (so o login valido do teste de CORS gravou) as 5 tentativas de forca bruta nao criaram sessao nenhuma
A força bruta deixa rastro no log, não no banco de sessão.
express-rate-limitresolve o balde pronto e ainda cuida doMapcrescendo sem parar — commaxele descarta entradas antigas, que sem isso é um vazamento de memória lento em produção.
Atras de nginx ou cloudflare,
req.socket.remoteAddressé o do proxy, não o do visitante. Todo mundo passa pelo mesmo endereço e o limite por IP derruba o site inteiro. O caminho é lerX-Forwarded-For— mas só quando o proxy é conhecido e o header não vem do cliente, porque quem manda header é quem está atacando.
Limite por IP não é limite por pessoa. Quem sai da mesma rede — empresa, escola, VPN — compartilha o balde. Pelo mesmo motivo, limite por usuário autenticado é mais justo que limite por IP: o token já diz quem é.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 6: CORS, cabecalhos de seguranca e limite de taxa. // // O `helmet` e o `express-rate-limit` nao estao instalados, entao o exemplo // reproduz o que eles fazem: cabecalhos na resposta e um balde de tokens por // IP. O comportamento e o que importa, e ele e medido aqui. // // A ordem dos tres e a ordem da ameaca: o CORS decide se o navegador DEIXA a // resposta chegar na tela; o cabecalho decide o que o navegador FAZ com ela; // o rate limit decide quantas vezes o cliente pode chamar. const http = require('node:http'); const { createConnection } = require('mysql2/promise'); // ================================================== 1. os cabecalhos (helmet) // Cabecalho = instrucao para o navegador. Nenhum deles impede a requisicao de // chegar no servidor: eles valem o que valem na resposta que o navegador le. function cabecalhosDeSeguranca() { return { 'Content-Security-Policy': "default-src 'self'; script-src 'self'; object-src 'none'", 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'Referrer-Policy': 'no-referrer', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'X-Powered-By': '', }; } // ================================================ 2. o limite de taxa (balde) // Um balde por IP: `capacidade` requisicoes, e um "repouso" de `ms` que // devolve uma ficha a cada janela. Sem o repouso, uma rajada estoura o balde // inteiro mesmo com o limite "por minuto". function criarLimitador({ capacidade, repousoMs }) { const baldes = new Map(); // ip -> { fichas, ultimoRepouso } return function permite(ip) { const agora = Date.now(); let b = baldes.get(ip); if (!b) { b = { fichas: capacidade, ultimoRepouso: agora }; baldes.set(ip, b); } const passado = agora - b.ultimoRepouso; const devolvidas = Math.floor(passado / repousoMs); if (devolvidas > 0) { b.fichas = Math.min(capacidade, b.fichas + devolvidas); b.ultimoRepouso += devolvidas * repousoMs; } if (b.fichas <= 0) return { permitido: false, restantes: 0, esperaMs: repousoMs - (agora - b.ultimoRepouso) }; b.fichas -= 1; return { permitido: true, restantes: b.fichas, esperaMs: 0 }; }; } // ================================================== 3. o CORS, com origem // CORS e uma pergunta de UMA coisa: "o navegador pode mostrar a resposta desta // resposta para a origem X?". A resposta sai no header `Access-Control-*`. // // Regra que o exemplo aplica: uma origem por vez, vinda de uma lista. `*` e o // perigo — ele vale para qualquer site, e e o que o esquema "publicado no // git" usa. function cabecalhoCors(origemDoCliente, permitidas) { const resposta = { 'Vary': 'Origin', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '600', }; if (permitidas.includes(origemDoCliente)) { resposta['Access-Control-Allow-Origin'] = origemDoCliente; } // A origem nao permitida NAO recebe o header. O navegador bloqueia sozinho — // e o servidor nao precisa negar nada. return resposta; } async function main() { const c = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, multipleStatements: true, }); await c.query(` CREATE TABLE IF NOT EXISTS tb_login ( id INT AUTO_INCREMENT PRIMARY KEY, nm_user VARCHAR(40) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await c.query('TRUNCATE TABLE tb_login'); // As origens que o sistema aceita. Uma API sem interface propria aceita // pouca coisa; uma API usada pelo painel web aceita o dominio dele. const ORIGENS_PERMITIDAS = [ 'https://app.exemplo.com', 'http://localhost:8080', ]; // A senha que o exemplo aceita. O VALOR nao entra no arquivo: ele e gerado // na hora, a partir de uma frase fixa. Numa aplicacao de verdade este valor // vem do `.env` (ver o CONTRATO.md) e nunca do codigo — o que a aula // ensina e que um segredo escrito no arquivo vaza no git. Aqui ele e // sintetico de proposito, para o exemplo rodar sem configuracao. const SENHA_EXEMPLO = 'exemplo-' + 'do-' + 'material'; const SENHA_ERRADA = SENHA_EXEMPLO + '-errada'; let permite = criarLimitador({ capacidade: 3, repousoMs: 1000 }); // O IP do cliente. Atras de nginx ou cloudflare, o que chega em // `remoteAddress` e o do PROXY, e nao o do visitante: e por isso que o // header `X-Forwarded-For` existe, e por isso que ele so pode ser lido // quando o proxy e conhecido. const ipDoCliente = (req) => req.socket.remoteAddress || '127.0.0.1'; const servidor = http.createServer(async (req, res) => { const origem = req.headers.origin || ''; const cab = cabecalhosDeSeguranca(); Object.assign(cab, cabecalhoCors(origem, ORIGENS_PERMITIDAS)); // O preflight nao chega a tocar o banco: e so uma pergunta de metodo. if (req.method === 'OPTIONS') { res.writeHead(204, cab); return res.end(); } // O rate limit vem ANTES do trabalho. Limitar depois de gastar MySQL // deixa o ataque pagar o preco completo antes de ser barrado. const limite = permite(ipDoCliente(req)); if (!limite.permitido) { res.writeHead(429, { ...cab, 'Retry-After': String(Math.ceil(limite.esperaMs / 1000)), }); return res.end(JSON.stringify({ erro: 'limite de requisicoes excedido' })); } res.setHeader('X-RateLimit-Remaining', String(limite.restantes)); const partes = []; for await (const p of req) partes.push(p); const dados = JSON.parse(Buffer.concat(partes).toString('utf8') || '{}'); if (req.url === '/login' && req.method === 'POST') { const valido = Boolean(dados.usuario) && dados.senha === SENHA_EXEMPLO; if (valido) { await c.execute('INSERT INTO tb_login (nm_user) VALUES (?)', [dados.usuario]); } res.writeHead(valido ? 200 : 401, cab); return res.end(JSON.stringify( valido ? { ok: true, usuario: dados.usuario } : { erro: 'credencial invalida' })); } res.writeHead(404, cab); res.end(JSON.stringify({ erro: 'rota nao encontrada' })); }); await new Promise((r) => servidor.listen(0, '127.0.0.1', r)); const base = 'http://127.0.0.1:' + servidor.address().port; console.log('servidor com CORS, cabecalhos e rate limit em ' + base); console.log('a porta muda a cada execucao: e o listen(0) pedindo uma livre'); // ---------------------------------------------------------- os cabecalhos console.log('\n--- 1. os cabecalhos que a resposta traz ---'); const comCab = await fetch(base + '/inexistente', { headers: { Origin: 'https://app.exemplo.com' }, }); console.log('status: ' + comCab.status); for (const [nome, valor] of Object.entries(cabecalhosDeSeguranca())) { console.log(' ' + nome.padEnd(28) + (valor === '' ? '(vazio, remove o header)' : valor)); } console.log('X-Powered-By vazio e o padrao: ele declara a tecnologia usada'); console.log('o cabecalho de CORS vem separado, porque depende da origem pedida:'); const cors = cabecalhoCors('https://app.exemplo.com', ORIGENS_PERMITIDAS); for (const [nome, valor] of Object.entries(cors)) { console.log(' ' + nome.padEnd(28) + valor); } // ------------------------------------------------------------------ CORS console.log('\n--- 2. CORS: origem permitida e origem de qualquer site ---'); const comCors = await fetch(base + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: 'https://app.exemplo.com' }, body: JSON.stringify({ usuario: 'ana', senha: SENHA_EXEMPLO }), }); console.log('POST com origem permitida -> status ' + comCors.status + ', Access-Control-Allow-Origin: ' + comCors.headers.get('access-control-allow-origin')); const comCors2 = await fetch(base + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: 'https://site-estranho.example' }, body: JSON.stringify({ usuario: 'ana', senha: SENHA_ERRADA }), }); console.log('POST com origem nao permit. -> status ' + comCors2.status + ', Access-Control-Allow-Origin: ' + (comCors2.headers.get('access-control-allow-origin') ?? '(ausente)')); console.log('mesma rota, mesma senha errada, mesmo status: o servidor se comportou igual'); console.log('a UNICA diferenca e o header ausente — e e ele que faz o navegador'); console.log('nao mostrar a resposta para o site de origem nao permitida'); console.log('CORS nao protege o banco: ele protege a LEITURA da resposta no navegador'); const preflight = await fetch(base + '/login', { method: 'OPTIONS', headers: { Origin: 'https://app.exemplo.com', 'Access-Control-Request-Method': 'POST', 'Access-Control-Request-Headers': 'Content-Type', }, }); console.log('\npreflight OPTIONS -> status ' + preflight.status + ' (o navegador pergunta antes de mandar o POST)'); console.log('Access-Control-Max-Age: ' + preflight.headers.get('access-control-max-age') + ' — o navegador guarda a resposta do preflight por 600s'); // ------------------------------------------------------------ rate limit // O balde e recomecado aqui, com nome proprio, para que a sequencia abaixo // comece do zero. As requisicoes de CORS acima JA consumiram as fichas do // balde deste IP — sem recomecar, o teste comecaria ja em 429 e nao // mostraria a transicao. console.log('\n--- 3. rate limit: balde novo, 3 fichas por IP ---'); console.log('as requisicoes de CORS acima consumiram o balde anterior;'); console.log('agora um balde limpo, com capacidade 3 e 1 ficha por segundo'); permite = criarLimitador({ capacidade: 3, repousoMs: 1000 }); let restanteNoCabecalho; const tentativa = (n) => fetch(base + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ usuario: 'bruno', senha: `${'errada'}-${n}` }), }); for (let i = 1; i <= 5; i++) { const r = await tentativa(i); restanteNoCabecalho = r.headers.get('x-ratelimit-remaining'); console.log('tentativa ' + i + ' -> ' + r.status + ' restantes=' + (restanteNoCabecalho ?? '(sem header)') + (r.status === 429 ? ' Retry-After=' + r.headers.get('retry-after') + 's' : '')); } console.log('as tres primeiras chegaram ao login; da quarta em diante, 429'); console.log('o contador de fichas no header vai 2, 1, 0 e so entao o 429'); const [tentativas] = await c.query('SELECT COUNT(*) AS n FROM tb_login'); console.log('\nsessoes criadas no banco: ' + tentativas[0].n + ' (so o login valido do teste de CORS gravou)'); console.log('as 5 tentativas de forca bruta nao criaram sessao nenhuma'); await new Promise((r) => servidor.close(r)); console.log('\nservidor encerrado com close().'); await c.end(); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
servidor com CORS, cabecalhos e rate limit em http://127.0.0.1:46037 a porta muda a cada execucao: e o listen(0) pedindo uma livre --- 1. os cabecalhos que a resposta traz --- status: 404 Content-Security-Policy default-src 'self'; script-src 'self'; object-src 'none' X-Content-Type-Options nosniff X-Frame-Options DENY Referrer-Policy no-referrer Strict-Transport-Security max-age=31536000; includeSubDomains X-Powered-By (vazio, remove o header) X-Powered-By vazio e o padrao: ele declara a tecnologia usada o cabecalho de CORS vem separado, porque depende da origem pedida: Vary Origin Access-Control-Allow-MethodsGET, POST, OPTIONS Access-Control-Allow-HeadersContent-Type, Authorization Access-Control-Max-Age 600 Access-Control-Allow-Origin https://app.exemplo.com --- 2. CORS: origem permitida e origem de qualquer site --- POST com origem permitida -> status 200, Access-Control-Allow-Origin: https://app.exemplo.com POST com origem nao permit. -> status 401, Access-Control-Allow-Origin: (ausente) mesma rota, mesma senha errada, mesmo status: o servidor se comportou igual a UNICA diferenca e o header ausente — e e ele que faz o navegador nao mostrar a resposta para o site de origem nao permitida CORS nao protege o banco: ele protege a LEITURA da resposta no navegador preflight OPTIONS -> status 204 (o navegador pergunta antes de mandar o POST) Access-Control-Max-Age: 600 — o navegador guarda a resposta do preflight por 600s --- 3. rate limit: balde novo, 3 fichas por IP --- as requisicoes de CORS acima consumiram o balde anterior; agora um balde limpo, com capacidade 3 e 1 ficha por segundo tentativa 1 -> 401 restantes=2 tentativa 2 -> 401 restantes=1 tentativa 3 -> 401 restantes=0 tentativa 4 -> 429 restantes=(sem header) Retry-After=1s tentativa 5 -> 429 restantes=(sem header) Retry-After=1s as tres primeiras chegaram ao login; da quarta em diante, 429 o contador de fichas no header vai 2, 1, 0 e so entao o 429 sessoes criadas no banco: 1 (so o login valido do teste de CORS gravou) as 5 tentativas de forca bruta nao criaram sessao nenhuma servidor encerrado com close().
Segredo, variável de ambiente e log
Onde o segredo mora, e o que pode ser versionado
A senha do banco nunca entra no git. Nem no .js, nem no .md, nem no
package.json. O que entra é o nome da variável — e nome não é segredo.
A distinção que evita quase todo acidente:
| Coisa | Onde vive | Vai no git |
|---|---|---|
DB_USER | .env | sim, é um nome |
DB_NAME | .env | sim, é um nome |
DB_PASS | .env | nunca, é o valor |
Quando alguém commitou a senha, quase sempre não foi por querer: foi por
git add . pegando o .env inteiro, porque a linha do arquivo se parece com
uma linha de configuração e parece inofensiva. Por isso a regra não é
"escreva a senha com cuidado", é que o arquivo não pode estar no caminho do
git.
A variável de ambiente e process.env
variável de ambiente é um par nome/valor que pertence ao processo do sistema
operacional, não ao seu código. O Node entrega todas elas em um objeto só,
process.env, que existe antes da primeira linha do seu arquivo rodar.
typeof process.env.DB_PASS devolve 'string' quando a variável chegou e
'undefined' quando não chegou — e a variável ausente não é erro, é um
valor como outro. É esse o motivo de o erro clássico existir: o código segue,
o valor vai para a consulta como undefined, e a falha aparece vinte linhas
depois, em outro arquivo, com outra mensagem.
process.env não é só o seu .env. São também todas as variáveis do
sistema, e por isso Object.keys(process.env) devolve uma lista longa que não
tem nada a ver com o seu projeto. As três verificações que valem:
typeof process.env.DB_PASS // 'string' ou 'undefined' 'DB_PASS' in process.env // true ou false Object.keys(process.env).length // quantas o processo inteiro tem
NODE_ENV é a variável que o Node não cria e todo projeto usa. Ela não é
opcional no código: é a que decide se o programa abre log de detalhe, se
mostra a página de erro ou se desliga o que só pode rodar em máquina de
desenvolvimento. Quando ela falta, o padrão seguro é produção, não
desenvolvimento — o contrário de deixar o padrão explícito, que expõe
detalhe em produção por acidente.
O .env não entra no process.env sozinho. Quem faz isso é o dotenv, na
primeira linha do programa:
require('dotenv').config(); // le o .env e escreve dentro de process.env
Sem essa chamada, process.env é só o ambiente do sistema — e o .env fica
sentado ao lado do programa, sem ser lido.
não versionar .env: o .gitignore do segredo
O .gitignore do segredo é um arquivo de três linhas, e cada linha resolve um
caso diferente:
.env .env.* !env.exemplo
.envrecusa o arquivo de credencial..env.*recusa todo arquivo que comece com.env.— inclusive
.env.producao, que é o jeito mais comum de vazar: o .env está
protegido e o .env.producao, que tem a senha de verdade, não.
!env.exemplovolta a aceitar o modelo. O!é negação, e vem depois:
regra do git mais abaixo vence a de cima.
Esse modelo é o que vai no git, com os nomes e os valores vazios:
DB_HOST= DB_PORT= DB_USER= DB_PASS= DB_NAME= NODE_ENV=development
Quem clona o repositório copia env.exemplo para .env e preenche só na sua
máquina. O valor da senha nunca aparece no arquivo que os outros leem.
O .gitignore não é um pedido, é uma regra que o git aplica sozinho. Os
comandos que provam isso:
git check-ignore -v .env # qual regra recusa, e em que linha do .gitignore git ls-files .env # o que o git realmente versiona git ls-files env.exemplo # o modelo tem de aparecer aqui
git check-ignore -v sai com 0 quando a regra existe e 1 quando não
existe. Saída 1 para .env é defeito de configuração, não detalhe: o
arquivo está pronto para entrar no próximo git add.
.gitignoresó vale para arquivo ainda não commitado. Se o.enventrou
no histórico, apagar a linha do
.gitignorenão resolve: o valor continua
em todos os commits antigos, e qualquer pessoa com o repositório clonado o
lê. A partir daí a correção é reescrever o histórico e trocar a senha.
A variável que faltou: undefined e a falha cedo
Ler process.env direto em todo lugar espalha o problema: o erro de
configuração aparece onde a conexão é aberta, sem nome e sem arquivo. A
alternativa é uma função que exige a variável e nomeia o erro:
class ErroDeConfiguracao extends Error { constructor(nome) { super('a variavel de ambiente ' + nome + ' nao chegou no processo'); this.code = 'ENV_AUSENTE'; this.nomeVariavel = nome; } } function exigir(amb, nome) { const valor = amb[nome]; if (!valor) throw new ErroDeConfiguracao(nome); return valor; }
exigir(amb, nome) devolve o valor, ou interrompe o programa com um erro que
carrega o nome da variável. Quem chama nunca chega a abrir a conexão com
uma variável faltando — a falha é cedo e nomeada, e a mensagem diz o arquivo
onde o valor deveria estar:
const config = { host: exigir(process.env, 'DB_HOST'), user: exigir(process.env, 'DB_USER'), password: exigir(process.env, 'DB_PASS'), // para aqui se faltar };
A diferença aparece quando a checagem não existe. Sem exigir, o driver
recebe password: undefined e responde ER_ACCESS_DENIED_ERROR. O programa
então procura o problema no driver, no usuário do banco, na permissão da
conta — e o .env aberto na frente, que é a causa, passa despercebido.
descreverValor(valor) é a função que permite olhar o ambiente inteiro sem
vazar nada. Ela devolve uma frase, nunca o valor:
descreverValor(process.env.DB_PASS) // 'chegou (20 caracteres, valor nao impresso)' descreverValor(process.env.NODE_ENV) // 'NAO CHEGOU'
O comprimento muda entre máquinas, o conteúdo não aparece. É assim que se
imprime o estado do .env inteiro sem transformar a saída do programa em um
vazamento.
log de erro: duas leituras, dois formatos
Quem lê um erro são duas audiências, e cada uma precisa de um formato:
| Regra | O que proíbe |
|---|---|
stack trace no console no lado do operador | nada — o operador precisa do arquivo e da linha |
não vazar caminho do arquivo na resposta | stack, caminho absoluto, nome de tabela e SQL |
dado sensível no log | o valor da senha, o corpo inteiro do pedido, o token |
informar erro ao cliente | traceback cru; o cliente precisa de código e de frase |
O stack trace no console é a stack de erro.stack: arquivo, linha e a
cadeia de chamadas. Ela é a ferramenta mais valiosa do operador e a pior
coisa que pode chegar no navegador. Por isso a pilha vai para o arquivo de
log, e a resposta HTTP leva o par de baixo.
respostaDeErro(erro) monta o corpo que o cliente recebe:
const FRASES = { ER_DUP_ENTRY: 'esse registro ja existe', ER_NO_SUCH_TABLE: 'tabela indisponivel neste momento', ENV_AUSENTE: 'falta configuracao no servidor', }; function respostaDeErro(erro) { return { erro: erro.code || 'ERRO_DESCONHECIDO', mensagem: FRASES[erro.code] || 'falha interna, o time foi avisado', }; }
respostaDeErro(erro) devolve erro.code e uma frase traduzida. O
ER_DUP_ENTRY é o que o front-end compara para decidir se oferece "tentar de
novo" ou "cadastre outro registro" — e é por isso que logging sozinho não
serve: quem consome o log em produção precisa do código do erro, não de
uma frase que pode vir traduzida. erro.message sozinho não decide nada,
porque a mensagem muda entre versões do banco e não é estável para comparar.
registrar(nivel, mensagem, extra) monta a linha de log e a devolve em vez de
imprimir direto, que é o que permite conferir o que foi registrado:
function registrar(nivel, mensagem, extra) { const hora = new Date().toISOString().slice(11, 19) + ' UTC'; let linha = hora + ' ' + nivel + ' ' + mensagem; if (extra !== undefined) linha += ' ' + JSON.stringify(extra); return linha; }
logDoOperador(nivel, mensagem, detalhe) é a chamada que junta as duas: ela
guarda a linha numa lista e devolve, para o log de erro ir inteiro.
nomesDoEnv(caminho) é o espelho do dotenv, reduzido ao mínimo: lê o
arquivo e devolve só o nome de cada variável e se ela tem valor. O valor é
lido da linha e descartado ali mesmo — o que volta para o programa é
{ nome, preenchida }.
gitComandos(args) roda um comando do git na raiz do projeto e devolve
{ ok, linhas }. O exemplo a usa para perguntar ao git, em vez de afirmar na
frase que o .env está protegido.
dado sensível no loginclui mais do que senha: cabeçalhoAuthorization,
document.cookie, número de cartão, CPF e o corpo de um pedido de login.
A prática que resolve é o oposto de "filtrar o que não deve aparecer":
registrar uma lista de campos, nunca o objeto inteiro.
log({ usuario, id })não vaza;log(requisicao.body)vaza tudo o que o
cliente mandou.
O banco guarda o nome: tb_segredo_config
A tabela de configuração de segredos guarda o nome da variável e o que
fazer com ela. Não tem coluna para o valor — e é a ausência da coluna que
torna o vazamento impossível, em vez de improvável.
| Coluna | O que guarda |
|---|---|
id | chave da linha |
nm_variavel | o nome, com UNIQUE: um segredo, um lugar |
ds_tipo | nome (versionável) ou segredo (só no .env) |
fl_obrigatoria | se o programa pode subir sem ela |
ds_verificacao | como confirmar que chegou, sem mostrar o valor |
Com essa tabela a auditoria fica possível: o código lê nm_variavel, procura
o valor no ambiente e grava o nome em log. Uma variável nova entra na tabela
antes de entrar no código, e o .env.example é gerado a partir dela.
A mesma disciplina vale para o log. A verificação final do exemplo compara
cada linha registrada contra o valor verdadeiro do segredo e imprime
quantas casaram — nunca a linha que casou:
const vazou = LINHAS_DE_LOG.filter((l) => l.includes(process.env.DB_PASS)); console.log('linhas que contem o valor de DB_PASS: ' + vazou.length);
LINHAS_DE_LOG.filter(...) devolve as linhas que casaram, e o programa joga
a lista fora e imprime o tamanho. É o teste que roda a cada deploy, e é o que
pega o vazamento antes do vazamento.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 6: segredo, variavel de ambiente e log. // // A aula responde tres perguntas, e nesta ordem: // // 1. o que PODE ir no git: o NOME do `segredo`. o que nunca vai: o VALOR. // O `.gitignore do segredo` e a regra que garante isso, e o // `não versionar .env` e o efeito que ela produz. // 2. o que acontece quando a `variável de ambiente` nao chegou no processo: // o `undefined` silencioso, e a `falha cedo` que o troca por um erro // com o NOME da variavel // 3. quem le o `log`: o operador recebe o `stack trace no console` com // arquivo e linha, e o `log de erro` completo; o cliente recebe so o // `informar erro ao cliente` — `não vazar caminho do arquivo` e // `dado sensível no log` valem para os dois lados do `logging` // // Nenhuma credencial esta escrita neste arquivo: todo valor vem de // `process.env`. O unico arquivo lido em disco e o `.env`, e dele sao lidos // SO os nomes das variaveis — o valor e lido e devolvido descartado. // // O `.env` ja esta carregado no `process.env` quando este arquivo comeca: quem // leu foi o harness com `-r`, e num projeto real quem le e o `dotenv`, na // primeira linha do programa. Este exemplo nao repete essa chamada — ele // ensina o que vem DEPOIS dela. const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { createConnection } = require('mysql2/promise'); // A raiz do material: tres niveis acima deste arquivo. O `git` e chamado com // essa pasta como diretorio, porque `git check-ignore` so responde quando o // caminho perguntado esta dentro da arvore. const RAIZ = path.join(__dirname, '..', '..', '..'); const ARQUIVO_ENV = path.join(RAIZ, '.env'); // ================================================ 1. o que tem no ambiente // Erro de configuracao com nome proprio: e o que separa "o banco recusou // minha senha" de "a senha nunca chegou". A mensagem leva o NOME da variavel e // o arquivo onde ela deveria estar — nunca o valor, porque quem le erro de // configuracao e justamente quem nao pode ver a senha. class ErroDeConfiguracao extends Error { constructor(nome) { super('a variavel de ambiente ' + nome + ' nao chegou no processo — ' + 'ela esta vazia ou ausente no arquivo .env'); this.code = 'ENV_AUSENTE'; this.nomeVariavel = nome; } } // `exigir(amb, nome)` devolve o valor ou para o programa aqui. A assinatura e o // contrato inteiro da aula: quem chama `exigir` nunca chega a abrir conexao // com uma variavel faltando. function exigir(amb, nome) { const valor = amb[nome]; if (!valor) throw new ErroDeConfiguracao(nome); return valor; } // `descreverValor(valor)` responde "chegou?" sem mostrar o que chegou. E o que // permite imprimir o estado do ambiente inteiro sem transformar a saida em // vazamento: o comprimento muda entre maquinas, o conteudo nao aparece. function descreverValor(valor) { if (valor === undefined) return 'NAO CHEGOU'; if (valor === '') return 'veio VAZIA'; return 'chegou (' + String(valor).length + ' caracteres, valor nao impresso)'; } // `nomesDoEnv(caminho)` le o `.env` e devolve so o nome de cada variavel e se // ela tem valor. O valor e lido da linha e descartado ali mesmo: o que volta // para o programa e o nome. function nomesDoEnv(caminho) { let bruto; try { bruto = fs.readFileSync(caminho, 'utf8'); } catch (erro) { return { erro: erro.code, nomes: [] }; } const nomes = []; for (const linha of bruto.split('\n')) { const limpa = linha.trim(); if (!limpa || limpa.startsWith('#')) continue; const corte = limpa.indexOf('='); if (corte === -1) continue; nomes.push({ nome: limpa.slice(0, corte).trim(), preenchida: limpa.slice(corte + 1).trim() !== '', }); } return { erro: null, nomes }; } // ============================================================ 2. o log // // Uma linha por evento, e nenhum outro lugar do programa escreve log. A funcao // DEVOLVE a linha em vez de imprimir: e assim que da para guardar em lista e // conferir o que entrou nela — a unica forma de provar "este log nao tem // segredo" em vez de confiar que nao tem. // // A hora vem do relogio e muda a cada execucao, como a porta do servidor no // exemplo da aula 1. O formato da linha nao muda. function registrar(nivel, mensagem, extra) { const hora = new Date().toISOString().slice(11, 19) + ' UTC'; let linha = hora + ' ' + nivel + ' ' + mensagem; if (extra !== undefined) linha += ' ' + JSON.stringify(extra); return linha; } // Tudo que foi para o log do operador nesta execucao. E a lista que a // verificacao do fim do exemplo percorre. const LINHAS_DE_LOG = []; // O operador recebe a pilha inteira. O cliente nunca recebe. function logDoOperador(nivel, mensagem, detalhe) { const linha = registrar(nivel, mensagem, detalhe); LINHAS_DE_LOG.push(linha); return linha; } // `respostaDeErro(erro)` monta o corpo que o cliente recebe quando a consulta // falha: o codigo, que o front-end pode comparar, e uma frase que o usuario // le. Sem `stack`, sem caminho de arquivo, sem SQL, sem nome de tabela. const FRASES_CONHECIDAS = { ER_DUP_ENTRY: 'esse registro ja existe', ER_NO_SUCH_TABLE: 'tabela indisponivel neste momento', ER_ACCESS_DENIED_ERROR: 'credencial recusada pelo banco', ER_ACCESS_DENIED_NO_PASSWORD_ERROR: 'credencial recusada pelo banco', ER_DBACCESS_DENIED_ERROR: 'base de dados inexistente', ENV_AUSENTE: 'falta configuracao no servidor', }; function respostaDeErro(erro) { return { erro: erro.code || 'ERRO_DESCONHECIDO', mensagem: FRASES_CONHECIDAS[erro.code] || 'falha interna, o time foi avisado', }; } // ============================================ 3. o git respondendo se o `.env` // entra. `gitComandos(args)` roda o git na raiz do material e devolve as // linhas. Sem git na maquina, o exemplo avisa e segue: a prova do git e uma // parte da aula, nao a aula inteira. function gitComandos(args) { const r = spawnSync('git', args, { cwd: RAIZ, encoding: 'utf8' }); if (r.error) return { ok: false, linhas: ['(git indisponivel nesta maquina)'] }; return { ok: r.status === 0, linhas: (r.stdout + r.stderr).split('\n').map((l) => l.trim()).filter(Boolean), }; } async function main() { // A conexao que o `process.env` montou. Todo valor abaixo vem de la: e o que // prova, na pratica, que o `.env` alimenta o programa. const banco = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, multipleStatements: true, }); console.log('--- 1. o que chegou no ambiente (nomes, nunca valores) ---'); const [versao] = await banco.query('SELECT VERSION() AS versao'); console.log('o .env montou a conexao, e o banco respondeu: ' + versao[0].versao); console.log('typeof process.env.NODE_ENV: ' + (typeof process.env.NODE_ENV === 'undefined' ? 'undefined' : 'string')); console.log('NODE_ENV: ' + descreverValor(process.env.NODE_ENV) + ' — o programa le como "' + (process.env.NODE_ENV || 'desenvolvimento') + '"'); console.log('o "desenvolvimento" acima e um valor de fallback escrito no codigo,'); console.log('nao veio de variavel nenhuma — e por isso ele nunca mente.'); const doEnv = nomesDoEnv(ARQUIVO_ENV); if (doEnv.erro) { console.log('o arquivo .env nao pode ser lido (' + doEnv.erro + ')'); } else { console.log('\nnome no arquivo .env no processo estado do valor'); for (const linha of doEnv.nomes) { console.log(' ' + linha.nome.padEnd(13) + (linha.preenchida ? 'preenchida ' : 'VAZIA ') + (linha.nome in process.env ? 'chegou ' : 'AUSENTE ') + descreverValor(process.env[linha.nome])); } console.log('\nesquerda: o NOME da variavel, que e o que vai no git.'); console.log('direita: o VALOR, que existe so nesta maquina e so em memoria.'); } console.log('Object.keys(process.env).length: ' + Object.keys(process.env).length + ' variaveis no processo inteiro, nao so as do arquivo .env'); // ------------------------------------------------------ a prova do git console.log('\n--- 2. o git respondendo se o .env entra ---'); const ignorado = gitComandos(['check-ignore', '-v', '.env']); console.log('git check-ignore -v .env (saida ' + (ignorado.ok ? 0 : 1) + '):'); for (const l of ignorado.linhas) console.log(' ' + l); console.log(ignorado.ok ? ' a regra acima manda o git recusar o arquivo: ele nem entra no commit' : ' o arquivo NAO esta sendo recusado, e isso e defeito de configuracao'); const rastreado = gitComandos(['ls-files', '.env']); console.log('git ls-files .env: ' + (rastreado.linhas.length ? rastreado.linhas.join(', ') : '(nenhuma linha) — o .env nao esta versionado')); const modelo = gitComandos(['ls-files', 'env.exemplo']); console.log('git ls-files env.exemplo: ' + (modelo.linhas.length ? modelo.linhas.join(', ') : '(nenhuma linha) — o modelo com os NOMES deveria estar versionado')); // ------------------------------------ o que acontece quando falta a variavel console.log('\n--- 3. a variavel que faltou, e as duas respostas ---'); console.log('mesma maquina, mesmo .env, uma variavel apagada do ambiente:'); const semSegredo = Object.assign({}, process.env); delete semSegredo.DB_PASS; console.log("typeof semSegredo.DB_PASS: " + typeof semSegredo.DB_PASS + ' — a variavel sumiu, e o valor gravado no arquivo continua intoc'); // (a) com a checagem antes de abrir a conexao let config = null; try { config = { host: exigir(semSegredo, 'DB_HOST'), port: Number(exigir(semSegredo, 'DB_PORT')), user: exigir(semSegredo, 'DB_USER'), password: exigir(semSegredo, 'DB_PASS'), database: exigir(semSegredo, 'DB_NAME'), }; } catch (erro) { console.error(logDoOperador('ERRO', erro.code + ' — ' + erro.nomeVariavel + ' ausente; o programa parou antes de abrir conexao', { pilha: erro.stack })); console.log(' com checagem: ' + erro.code + ' — ' + erro.message); console.log(' o que o cliente recebe: ' + JSON.stringify(respostaDeErro(erro))); } // (b) sem a checagem, deixando o driver descobrir sozinho const cRuim = await createConnection({ host: semSegredo.DB_HOST, port: Number(semSegredo.DB_PORT), user: semSegredo.DB_USER, password: semSegredo.DB_PASS, // undefined: o driver le como "sem senha" database: semSegredo.DB_NAME, }).then((c) => ({ ok: c }), (erro) => ({ erro })); if (config) console.log(' (a nao parou: o .env tem a variavel)'); if (cRuim.erro) { const erro = cRuim.erro; console.error(logDoOperador('ERRO', 'sem checagem — o driver recusou: ' + erro.code + ' / errno ' + erro.errno, { pilha: erro.stack })); console.log(' sem checagem: ' + erro.code + ' (errno ' + erro.errno + ')'); console.log(' o que o cliente recebe: ' + JSON.stringify(respostaDeErro(erro))); console.log('\nas duas linhas a variavel ausente e a mesma: DB_PASS.'); console.log('a que tem checagem diz o NOME e o arquivo; a outra so diz que o'); console.log('banco recusou, e o programador procura no driver, no usuario, na'); console.log('permissao do banco — em tudo menos no .env, que e a causa.'); } if (cRuim.ok) await cRuim.ok.end(); // ================================================== 4. o log do operador // O banco guarda o NOME da variavel e o que fazer com ela. O valor nunca // entra: a tabela tem `nm_variavel` e nao tem coluna para valor nenhum. console.log('\n--- 4. o nome do segredo no banco, o valor fora ---'); await banco.query(` CREATE TABLE IF NOT EXISTS tb_segredo_config ( id INT AUTO_INCREMENT PRIMARY KEY, nm_variavel VARCHAR(60) NOT NULL UNIQUE, ds_tipo VARCHAR(20) NOT NULL, fl_obrigatoria TINYINT NOT NULL DEFAULT 1, ds_verificacao VARCHAR(90) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); // `IF NOT EXISTS` so verifica o NOME da tabela, nunca a forma dela: uma // tabela que ja existe com outra coluna passa sem erro e quebra no INSERT // logo depois, com um `ER_BAD_FIELD_ERROR` que nao tem nada a ver com a // aula. Esta guarda compara `SHOW COLUMNS` com o que o exemplo espera e // refaz a tabela quando a forma nao bate — e e o que torna o exemplo // idempotente mesmo depois de uma execucao antiga, ou de outro material que // criou a tabela com outro desenho. const [colunas] = await banco.query('SHOW COLUMNS FROM tb_segredo_config'); const nomesEsperados = ['id', 'nm_variavel', 'ds_tipo', 'fl_obrigatoria', 'ds_verificacao']; const bate = nomesEsperados.every((n) => colunas.some((c) => c.Field === n)); if (!bate) { console.log('a tabela ja existia com outra forma (' + colunas.map((c) => c.Field).join(', ') + '); refazendo para o desenho deste exemplo'); await banco.query('DROP TABLE tb_segredo_config'); await banco.query(` CREATE TABLE tb_segredo_config ( id INT AUTO_INCREMENT PRIMARY KEY, nm_variavel VARCHAR(60) NOT NULL UNIQUE, ds_tipo VARCHAR(20) NOT NULL, fl_obrigatoria TINYINT NOT NULL DEFAULT 1, ds_verificacao VARCHAR(90) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); } await banco.query('DELETE FROM tb_segredo_config'); // `ds_tipo` separa as duas coisas que sempre se confundem: um NOME, que pode // ser versionado, e um VALOR, que nao pode. const cadastro = [ ['DB_HOST', 'nome', 0, 'endereco do servidor; no .env.example ja vem preenchido'], ['DB_PORT', 'nome', 0, 'porta do servidor; o padrao do MySQL e 3306'], ['DB_USER', 'nome', 0, 'nome do usuario do banco; este e o que pode ir no git'], ['DB_NAME', 'nome', 0, 'nome do banco; o mesmo arquivo serve a teste e producao'], ['NODE_ENV', 'nome', 0, 'qual ambiente roda; padrao desenvolvimento quando falta'], ['DB_PASS', 'segredo', 1, 'valor da senha; NUNCA sai do .env, do log ou do banco'], ]; for (const [nome, tipo, obrig, texto] of cadastro) { await banco.query( 'INSERT INTO tb_segredo_config (nm_variavel, ds_tipo, fl_obrigatoria, ' + 'ds_verificacao) VALUES (?, ?, ?, ?)', [nome, tipo, obrig, texto]); } const [registradas] = await banco.query( 'SELECT nm_variavel, ds_tipo, fl_obrigatoria, ds_verificacao ' + 'FROM tb_segredo_config ORDER BY id'); for (const linha of registradas) { console.log(' ' + linha.nm_variavel.padEnd(10) + linha.ds_tipo.padEnd(10) + (linha.fl_obrigatoria ? 'obrigatoria ' : 'opcional ') + linha.ds_verificacao); } console.log('seis variaveis cadastradas, uma so marcada como segredo.'); console.log('a tabela nao tem coluna para o valor, e por isso o valor nao tem'); console.log('como entrar: guardar o nome deixa a auditoria possivel e o risco, zero.'); // O erro de verdade: cadastrar o mesmo nome duas vezes. E o que o log do // operador e a resposta do cliente fazem com o MESMO erro. console.log('\ncadastrando DB_PASS de novo, para gerar o erro de verdade:'); try { await banco.query( 'INSERT INTO tb_segredo_config (nm_variavel, ds_tipo, fl_obrigatoria, ' + 'ds_verificacao) VALUES (?, ?, ?, ?)', ['DB_PASS', 'segredo', 1, 'tentativa duplicada']); console.log(' gravou (inesperado: o UNIQUE de nm_variavel barraria)'); } catch (erro) { const pilha = String(erro.stack || ''); const resposta = JSON.stringify(respostaDeErro(erro)); console.error(logDoOperador('ERRO', 'cadastro de variavel recusado: ' + erro.code + ' / sqlState ' + erro.sqlState, { pilha })); console.log(' terminal (stderr, com a pilha inteira):'); console.log(' ' + pilha.split('\n')[0]); console.log(' pagina (stdout, o que o cliente recebe):'); console.log(' ' + resposta); console.log(' codigo no log: ' + erro.code + ' — comparavel, e ele que decide'); console.log(' a pilha aponta o arquivo do projeto: ' + pilha.includes('aula2.js')); console.log(' a pilha aponta a pasta do projeto: ' + pilha.includes('node-mysql')); console.log(' a resposta ao cliente tem caminho de arquivo: ' + /[\\/]/.test(resposta)); console.log(' a resposta ao cliente tem o nome da tabela: ' + resposta.includes('tb_segredo_config')); console.log(' a resposta ao cliente tem o SQL: ' + resposta.includes('INSERT')); } // A verificacao final: a lista de log e conferida contra o valor verdadeiro // do segredo sem que o valor apareca em lugar nenhum. E o teste que roda em // producao, em cada deploy, e que impede o vazamento antes do vazamento. const valorDoSegredo = process.env.DB_PASS; if (!valorDoSegredo) { console.log('\nDB_PASS esta vazia: nao ha valor para comparar com o log.'); } else { const vazou = LINHAS_DE_LOG.filter((l) => l.includes(valorDoSegredo)); console.log('\n--- 5. a verificacao do log contra o valor verdadeiro ---'); console.log('linhas de log nesta execucao: ' + LINHAS_DE_LOG.length); console.log('linhas que contem o valor de DB_PASS: ' + vazou.length); console.log('a comparacao foi feita contra o valor real, e o que voltou foi um'); console.log('numero — por isso o valor nao sai daqui nem se o log vazar mais tarde.'); } await banco.end(); console.log('\nexemplo encerrado.'); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- 1. o que chegou no ambiente (nomes, nunca valores) ---
o .env montou a conexao, e o banco respondeu: 10.11.14-MariaDB-0ubuntu0.24.04.1
typeof process.env.NODE_ENV: undefined
NODE_ENV: NAO CHEGOU — o programa le como "desenvolvimento"
o "desenvolvimento" acima e um valor de fallback escrito no codigo,
nao veio de variavel nenhuma — e por isso ele nunca mente.
nome no arquivo .env no processo estado do valor
DB_HOST preenchida chegou chegou (9 caracteres, valor nao impresso)
DB_PORT preenchida chegou chegou (4 caracteres, valor nao impresso)
DB_USER preenchida chegou chegou (9 caracteres, valor nao impresso)
DB_PASS preenchida chegou chegou (20 caracteres, valor nao impresso)
DB_NAME preenchida chegou chegou (15 caracteres, valor nao impresso)
esquerda: o NOME da variavel, que e o que vai no git.
direita: o VALOR, que existe so nesta maquina e so em memoria.
Object.keys(process.env).length: 111 variaveis no processo inteiro, nao so as do arquivo .env
--- 2. o git respondendo se o .env entra ---
git check-ignore -v .env (saida 0):
.gitignore:27:.env .env
a regra acima manda o git recusar o arquivo: ele nem entra no commit
git ls-files .env: (nenhuma linha) — o .env nao esta versionado
git ls-files env.exemplo: env.exemplo
--- 3. a variavel que faltou, e as duas respostas ---
mesma maquina, mesmo .env, uma variavel apagada do ambiente:
typeof semSegredo.DB_PASS: undefined — a variavel sumiu, e o valor gravado no arquivo continua intoc
com checagem: ENV_AUSENTE — a variavel de ambiente DB_PASS nao chegou no processo — ela esta vazia ou ausente no arquivo .env
o que o cliente recebe: {"erro":"ENV_AUSENTE","mensagem":"falta configuracao no servidor"}
sem checagem: ER_ACCESS_DENIED_ERROR (errno 1045)
o que o cliente recebe: {"erro":"ER_ACCESS_DENIED_ERROR","mensagem":"credencial recusada pelo banco"}
as duas linhas a variavel ausente e a mesma: DB_PASS.
a que tem checagem diz o NOME e o arquivo; a outra so diz que o
banco recusou, e o programador procura no driver, no usuario, na
permissao do banco — em tudo menos no .env, que e a causa.
--- 4. o nome do segredo no banco, o valor fora ---
DB_HOST nome opcional endereco do servidor; no .env.example ja vem preenchido
DB_PORT nome opcional porta do servidor; o padrao do MySQL e 3306
DB_USER nome opcional nome do usuario do banco; este e o que pode ir no git
DB_NAME nome opcional nome do banco; o mesmo arquivo serve a teste e producao
NODE_ENV nome opcional qual ambiente roda; padrao desenvolvimento quando falta
DB_PASS segredo obrigatoria valor da senha; NUNCA sai do .env, do log ou do banco
seis variaveis cadastradas, uma so marcada como segredo.
a tabela nao tem coluna para o valor, e por isso o valor nao tem
como entrar: guardar o nome deixa a auditoria possivel e o risco, zero.
cadastrando DB_PASS de novo, para gerar o erro de verdade:
terminal (stderr, com a pilha inteira):
Error: Duplicate entry 'DB_PASS' for key 'nm_variavel'
pagina (stdout, o que o cliente recebe):
{"erro":"ER_DUP_ENTRY","mensagem":"esse registro ja existe"}
codigo no log: ER_DUP_ENTRY — comparavel, e ele que decide
a pilha aponta o arquivo do projeto: true
a pilha aponta a pasta do projeto: true
a resposta ao cliente tem caminho de arquivo: false
a resposta ao cliente tem o nome da tabela: false
a resposta ao cliente tem o SQL: false
--- 5. a verificacao do log contra o valor verdadeiro ---
linhas de log nesta execucao: 3
linhas que contem o valor de DB_PASS: 0
a comparacao foi feita contra o valor real, e o que voltou foi um
numero — por isso o valor nao sai daqui nem se o log vazar mais tarde.
exemplo encerrado.