Capítulo 22, Intermediário
CORS
O CORS é uma regra do **navegador**, e não do servidor. Ele decide se o JavaScript de um site pode ler a resposta da sua API. Configurado errado, ou bloqueia o seu frontend, ou libera o seu backend para qualquer site.
O problema
Uma origem é a combinação de protocolo, domínio e porta. Um frontend em http://localhost:5173 e uma API em http://localhost:8000 são origens diferentes. Por segurança, o navegador bloqueia o JavaScript de uma origem de ler respostas de outra, a menos que o servidor diga que permite. Sem isso, você vê no console o erro "blocked by CORS policy".
O servidor permite respondendo com cabeçalhos Access-Control-Allow-*. No FastAPI, isso é um middleware:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["content-type"],
)
@app.get("/")
def home():
return {"mensagem": "CORS configurado"}
cliente = TestClient(app)
permitida = cliente.get("/", headers={"Origin": "http://localhost:5173"})
print(permitida.headers.get("access-control-allow-origin"), permitida.headers.get("access-control-allow-credentials"))
print(permitida.headers.get("vary"))
http://localhost:5173 true
Origin
A origem não permitida
Quando a origem não está na lista, o servidor ainda executa a requisição e responde 200, mas sem o cabeçalho de permissão. É o navegador que então esconde a resposta do JavaScript:
negada = cliente.get("/", headers={"Origin": "http://malicioso.com"})
print(negada.status_code, negada.headers.get("access-control-allow-origin"))
200 None
Isso significa que o CORS não é segurança do servidor. Quem chama de um script, do curl ou de outro servidor ignora o CORS por completo. Ele protege o usuário, no navegador, de um site malicioso que tente ler os dados da sessão dele na sua API. Autenticação e autorização continuam sendo trabalho da sua API.
O preflight
Para requisições "não simples" (um POST com JSON, um DELETE, cabeçalhos personalizados), o navegador antes faz uma pergunta com o método OPTIONS: "posso fazer esta requisição?". É o preflight. O middleware o responde sozinho:
preflight = cliente.options(
"/",
headers={
"Origin": "http://localhost:5173",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type",
},
)
print(preflight.status_code, preflight.headers["access-control-allow-methods"])
print(preflight.headers["access-control-max-age"])
negado = cliente.options(
"/", headers={"Origin": "http://localhost:5173", "Access-Control-Request-Method": "DELETE"}
)
print(negado.status_code, negado.text)
200 GET, POST
600
400 Disallowed CORS method
O DELETE não está em allow_methods, e o preflight devolve 400: o navegador nem envia a requisição real. O max-age diz por quanto tempo o navegador pode guardar a resposta do preflight.
A configuração perigosa: `*` com credenciais
Muito tutorial mostra allow_origins=["*"] junto de allow_credentials=True, para "fazer o erro sumir". A especificação proíbe essa combinação, porque ela entregaria a sessão do usuário a qualquer site. Veja o que o Starlette faz com ela:
app_aberto = FastAPI()
app_aberto.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app_aberto.get("/")
def aberto():
return {}
qualquer = TestClient(app_aberto).get("/", headers={"Origin": "http://site-qualquer.com"})
print(qualquer.headers.get("access-control-allow-origin"))
http://site-qualquer.com
Em vez de recusar, o Starlette devolve a origem de quem perguntou. O efeito prático é permitir qualquer site, com credenciais. É a configuração que mais vejo em código de produção, e ela anula a proteção do navegador.
A regra que eu sigo
Liste as origens exatas do seu frontend (
https://app.exemplo.com, maishttp://localhost:5173em desenvolvimento), e leia a lista de uma variável de ambiente (capítulo 23). Use["*"]só para APIs públicas, sem cookies nem credenciais (uma API de dados abertos). Se a sua API usa cookies de sessão, ou se o seu frontend envia o token de uma forma que o navegador anexa sozinho, a lista exata é obrigatória.
| O que | Para que serve |
|---|---|
allow_origins | Quais sites podem ler a resposta |
allow_methods | Quais verbos são permitidos entre origens |
allow_headers | Quais cabeçalhos o frontend pode enviar |
allow_credentials | Se cookies e credenciais podem acompanhar a requisição |
allow_origin_regex | Origens por padrão (por exemplo, todos os subdomínios de exemplo.com) |
max_age | Por quanto tempo o navegador guarda a resposta do preflight |
Exercício 1
Duas origens a partir de um texto
Escreva montar_app(origens_csv), que receba um texto como "https://a.com,https://b.com" e devolva um app com CORS para exatamente essas origens. Confira que as duas são permitidas e uma terceira não.