PedroH2508 Claude Opus 5.5 commited on
Commit ·
9db9aad
1
Parent(s): 2bb327e
P2-1: recusa no servidor (403) de origem nao autorizada
Browse filesO proxy do *.hf.space acrescenta Access-Control-Allow-Origin refletindo qualquer
origem por cima do CORS do app (medido em 11/10/2026), entao ALLOWED_ORIGINS /
AIDA_VIDEO_CORS sozinhos nao impediam outro site de usar a API pelo navegador.
Agora a requisicao com Origin fora da lista recebe 403 antes da rota. Sem Origin
(curl, servidor) e o proprio host continuam passando; chamada interna isenta.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- site_api/app.py +4 -0
- site_api/origem.py +63 -0
- testes/test_origem.py +50 -0
site_api/app.py
CHANGED
|
@@ -54,6 +54,7 @@ from aida.utilitarios.imagem import ImagemInvalida # noqa: E402
|
|
| 54 |
from analisar_imagem import analisar_imagem # noqa: E402
|
| 55 |
from historico import buscar_analise_por_id # noqa: E402
|
| 56 |
from site_api.limite import LimitePorCliente, chamada_interna, identificar_cliente # noqa: E402
|
|
|
|
| 57 |
|
| 58 |
logging.basicConfig(level=logging.INFO)
|
| 59 |
logger = logging.getLogger("aida.api")
|
|
@@ -78,6 +79,9 @@ def _origens_permitidas():
|
|
| 78 |
|
| 79 |
|
| 80 |
CORS(app, resources={r"/*": {"origins": _origens_permitidas()}})
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
EXTENSOES_PERMITIDAS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".avif", ".bmp"}
|
| 83 |
|
|
|
|
| 54 |
from analisar_imagem import analisar_imagem # noqa: E402
|
| 55 |
from historico import buscar_analise_por_id # noqa: E402
|
| 56 |
from site_api.limite import LimitePorCliente, chamada_interna, identificar_cliente # noqa: E402
|
| 57 |
+
from site_api.origem import instalar as instalar_checagem_origem # noqa: E402
|
| 58 |
|
| 59 |
logging.basicConfig(level=logging.INFO)
|
| 60 |
logger = logging.getLogger("aida.api")
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
CORS(app, resources={r"/*": {"origins": _origens_permitidas()}})
|
| 82 |
+
# O proxy do *.hf.space reflete qualquer origem por cima do CORS acima: a recusa
|
| 83 |
+
# de verdade (403) é feita no servidor, com a mesma lista.
|
| 84 |
+
instalar_checagem_origem(app, "ALLOWED_ORIGINS", isenta=chamada_interna)
|
| 85 |
|
| 86 |
EXTENSOES_PERMITIDAS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".avif", ".bmp"}
|
| 87 |
|
site_api/origem.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Checagem de origem no SERVIDOR (auditoria P2-1).
|
| 2 |
+
|
| 3 |
+
O CORS do app não basta no Hugging Face: o proxy do ``*.hf.space`` acrescenta
|
| 4 |
+
``Access-Control-Allow-Origin`` refletindo qualquer origem por cima da resposta
|
| 5 |
+
(medido em 11/10/2026: com ``ALLOWED_ORIGINS`` definido, uma página de outro
|
| 6 |
+
domínio continuava lendo as respostas). Aqui a requisição de navegador vinda de
|
| 7 |
+
origem não autorizada é recusada com 403 antes de chegar à rota.
|
| 8 |
+
|
| 9 |
+
Regras, só quando a variável de origens está definida (``*`` ou vazia = desligado):
|
| 10 |
+
|
| 11 |
+
* sem cabeçalho ``Origin`` → passa. Navegadores mandam ``Origin`` em todo POST e em
|
| 12 |
+
chamadas ``fetch`` de outro domínio; quem não manda é cliente de servidor (curl,
|
| 13 |
+
o AIDA Video chamando o Core), que o CORS também nunca barrou;
|
| 14 |
+
* origem igual ao próprio host → passa (páginas servidas pelo Space, como ``/teste``);
|
| 15 |
+
* origem na lista → passa; qualquer outra, inclusive ``null`` → 403.
|
| 16 |
+
|
| 17 |
+
Não é autenticação: um cliente fora do navegador forja ``Origin`` à vontade. O que
|
| 18 |
+
isto impede é outro site usar a API pelo navegador dos visitantes dele.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
from urllib.parse import urlsplit
|
| 25 |
+
|
| 26 |
+
from flask import jsonify, request
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _normalizar(origem: str) -> str:
|
| 30 |
+
return origem.strip().rstrip("/").lower()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def origens_configuradas(variavel: str) -> set[str] | None:
|
| 34 |
+
"""Origens autorizadas, ou None se a checagem está desligada."""
|
| 35 |
+
valor = os.environ.get(variavel, "*").strip()
|
| 36 |
+
origens = {_normalizar(o) for o in valor.split(",") if o.strip()}
|
| 37 |
+
if not origens or "*" in origens:
|
| 38 |
+
return None
|
| 39 |
+
return origens
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def origem_permitida(origem: str | None, permitidas: set[str], host: str) -> bool:
|
| 43 |
+
if not origem:
|
| 44 |
+
return True
|
| 45 |
+
origem = _normalizar(origem)
|
| 46 |
+
if origem == "null":
|
| 47 |
+
return False
|
| 48 |
+
if urlsplit(origem).netloc and urlsplit(origem).netloc == (host or "").lower():
|
| 49 |
+
return True
|
| 50 |
+
return origem in permitidas
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def instalar(app, variavel: str, isenta=None) -> None:
|
| 54 |
+
"""Registra a checagem em ``app``. ``isenta(request)`` libera chamadas internas."""
|
| 55 |
+
|
| 56 |
+
@app.before_request
|
| 57 |
+
def _checar_origem():
|
| 58 |
+
permitidas = origens_configuradas(variavel)
|
| 59 |
+
if permitidas is None or (isenta is not None and isenta(request)):
|
| 60 |
+
return None
|
| 61 |
+
if origem_permitida(request.headers.get("Origin"), permitidas, request.host):
|
| 62 |
+
return None
|
| 63 |
+
return jsonify({"erro": "Origem não autorizada a usar esta API."}), 403
|
testes/test_origem.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""P2-1: recusa no servidor de requisição de navegador vinda de origem não autorizada."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from site_api.origem import origem_permitida, origens_configuradas
|
| 6 |
+
|
| 7 |
+
SITE = "https://www.aida-on.com.br,https://aida-on.com.br"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_desligada_sem_variavel_ou_com_asterisco(monkeypatch):
|
| 11 |
+
monkeypatch.delenv("ALLOWED_ORIGINS", raising=False)
|
| 12 |
+
assert origens_configuradas("ALLOWED_ORIGINS") is None
|
| 13 |
+
monkeypatch.setenv("ALLOWED_ORIGINS", "*")
|
| 14 |
+
assert origens_configuradas("ALLOWED_ORIGINS") is None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_regras_de_origem():
|
| 18 |
+
ok = {"https://www.aida-on.com.br", "https://aida-on.com.br"}
|
| 19 |
+
host = "aidaon-aida-api.hf.space"
|
| 20 |
+
assert origem_permitida(None, ok, host) # curl / servidor
|
| 21 |
+
assert origem_permitida("https://www.aida-on.com.br/", ok, host) # barra e caixa
|
| 22 |
+
assert origem_permitida("HTTPS://AIDA-ON.COM.BR", ok, host)
|
| 23 |
+
assert origem_permitida("https://aidaon-aida-api.hf.space", ok, host) # página /teste
|
| 24 |
+
assert not origem_permitida("https://evil.example", ok, host)
|
| 25 |
+
assert not origem_permitida("https://aida-on.com.br.evil.example", ok, host)
|
| 26 |
+
assert not origem_permitida("null", ok, host)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_rota_recusa_origem_estranha_e_aceita_o_site(cliente_api, monkeypatch):
|
| 30 |
+
monkeypatch.setenv("ALLOWED_ORIGINS", SITE)
|
| 31 |
+
monkeypatch.setenv("AIDA_CHAVE_INTERNA", "segredo-de-teste")
|
| 32 |
+
|
| 33 |
+
estranha = cliente_api.get("/saude", headers={"Origin": "https://evil.example"})
|
| 34 |
+
site = cliente_api.get("/saude", headers={"Origin": "https://www.aida-on.com.br"})
|
| 35 |
+
sem_origem = cliente_api.get("/saude")
|
| 36 |
+
estranha_post = cliente_api.post("/analisar", headers={"Origin": "https://evil.example"})
|
| 37 |
+
interna = cliente_api.post("/analisar", headers={"Origin": "https://evil.example",
|
| 38 |
+
"X-AIDA-Chave-Interna": "segredo-de-teste"})
|
| 39 |
+
|
| 40 |
+
assert estranha.status_code == 403 and "Origem" in estranha.get_json()["erro"]
|
| 41 |
+
assert site.status_code == 200
|
| 42 |
+
assert site.headers.get("Access-Control-Allow-Origin") == "https://www.aida-on.com.br"
|
| 43 |
+
assert sem_origem.status_code == 200
|
| 44 |
+
assert estranha_post.status_code == 403
|
| 45 |
+
assert interna.status_code != 403
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_rota_sem_variavel_nao_recusa(cliente_api, monkeypatch):
|
| 49 |
+
monkeypatch.delenv("ALLOWED_ORIGINS", raising=False)
|
| 50 |
+
assert cliente_api.get("/saude", headers={"Origin": "https://evil.example"}).status_code == 200
|