Pular para o conteúdo

    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:

    intermediario/cap22_cors.pylinhas 10 a 32
    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"))
    
    Saída
    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:

    intermediario/cap22_cors.pylinhas 37 a 38
    negada = cliente.get("/", headers={"Origin": "http://malicioso.com"})
    print(negada.status_code, negada.headers.get("access-control-allow-origin"))
    
    Saída
    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:

    intermediario/cap22_cors.pylinhas 43 a 57
    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)
    
    Saída
    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:

    intermediario/cap22_cors.pylinhas 62 a 78
    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"))
    
    Saída
    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, mais http://localhost:5173 em 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 quePara que serve
    allow_originsQuais sites podem ler a resposta
    allow_methodsQuais verbos são permitidos entre origens
    allow_headersQuais cabeçalhos o frontend pode enviar
    allow_credentialsSe cookies e credenciais podem acompanhar a requisição
    allow_origin_regexOrigens por padrão (por exemplo, todos os subdomínios de exemplo.com)
    max_agePor 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.