Spaces:
Sleeping
Sleeping
Claude
feat(sprint-S1.5+S1.6+S1.7): tests d'attaque ZIP slip / SSRF / CSRF + durcissement validate_http_url
02c6322 unverified | """Sprint S1.6 β Tests d'attaque SSRF (Server-Side Request Forgery). | |
| ``picarones.adapters.corpus._http.validate_http_url`` est censΓ© | |
| empΓͺcher un import IIIF/Gallica/HuggingFace malicieux de faire | |
| fetcher des ressources internes. | |
| Vecteurs couverts | |
| ----------------- | |
| 1. **SchΓ©mas non-HTTP** (``file://``, ``ftp://``, ``data:``) β | |
| dΓ©fense dΓ©jΓ annoncΓ©e. | |
| 2. **Localhost / loopback** (``http://127.0.0.1``, ``http://localhost``, | |
| ``http://[::1]``). | |
| 3. **MΓ©tadonnΓ©es cloud** (``http://169.254.169.254`` β AWS, | |
| ``http://metadata.google.internal`` β GCP). | |
| 4. **RΓ©seaux privΓ©s RFC 1918** (``10.0.0.0/8``, ``172.16.0.0/12``, | |
| ``192.168.0.0/16``). | |
| 5. **Lien local** (``169.254.0.0/16``). | |
| NB : la dΓ©fense statique ne suffit pas contre les attaques DNS | |
| rebinding ou les redirections HTTP qui pointent ensuite sur du | |
| loopback. Mais l'absence de dΓ©fense statique est un signal clair. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. SchΓ©mas non-HTTP (dΓ©jΓ censΓ© Γͺtre bloquΓ©) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestNonHttpSchemes: | |
| """``file://``, ``ftp://``, ``data:`` doivent lever | |
| ``ValueError``.""" | |
| def test_non_http_scheme_rejected(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| with pytest.raises(ValueError): | |
| validate_http_url(url) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. Loopback / localhost | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestLoopbackBlocked: | |
| """Un import IIIF qui pointe ``http://127.0.0.1:6379`` peut | |
| parler au Redis interne.""" | |
| def test_loopback_rejected(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| with pytest.raises(ValueError): | |
| validate_http_url(url) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. MΓ©tadonnΓ©es cloud (AWS / GCP) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCloudMetadataBlocked: | |
| """169.254.169.254 expose les credentials AWS IAM, GCP project | |
| metadata, etc. Doit Γͺtre refusΓ©.""" | |
| def test_cloud_metadata_rejected(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| with pytest.raises(ValueError): | |
| validate_http_url(url) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. RΓ©seaux privΓ©s RFC 1918 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestPrivateNetworksBlocked: | |
| """Dans une institution, l'app peut Γͺtre derriΓ¨re un reverse-proxy | |
| avec accès au réseau interne. Bloquer les IP privées par défaut.""" | |
| def test_private_ipv4_rejected(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| with pytest.raises(ValueError): | |
| validate_http_url(url) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 5. Lien-local IPv4 (hors AWS metadata, autres usages) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestLinkLocalBlocked: | |
| def test_link_local_rejected(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| with pytest.raises(ValueError): | |
| validate_http_url(url) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 6. Sanity : URLs publiques lΓ©gitimes acceptΓ©es | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestPublicURLsAccepted: | |
| def test_public_url_accepted(self, url: str) -> None: | |
| from picarones.adapters.corpus._http import validate_http_url | |
| # Ne lΓ¨ve pas β URL valide. | |
| validate_http_url(url) | |