PedroH2508 Claude Opus 5.5 commited on
Commit
9db9aad
·
1 Parent(s): 2bb327e

P2-1: recusa no servidor (403) de origem nao autorizada

Browse files

O 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>

Files changed (3) hide show
  1. site_api/app.py +4 -0
  2. site_api/origem.py +63 -0
  3. 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