Skip to content

Commit 03d4b2a

Browse files
committed
♻️(backend) stop allowing redirect in cors-proxy endpoint
The cors-proxy endpoint was allowing redirect when fetching the target url. This can be usefull if an image url has changed but also dangerous if an attacker wants to hide a SSRF behind a redirect.
1 parent 2556823 commit 03d4b2a

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

src/backend/core/api/viewsets.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,7 +1715,6 @@ def _validate_url_against_ssrf(self, url):
17151715
if not hostname:
17161716
raise drf.exceptions.ValidationError("Invalid hostname")
17171717

1718-
17191718
# Resolve hostname to IP address(es)
17201719
# Check all resolved IPs to prevent DNS rebinding attacks
17211720
try:
@@ -1804,14 +1803,15 @@ def cors_proxy(self, request, *args, **kwargs):
18041803
"User-Agent": request.headers.get("User-Agent", ""),
18051804
"Accept": request.headers.get("Accept", ""),
18061805
},
1806+
allow_redirects=False,
18071807
timeout=10,
18081808
)
1809+
response.raise_for_status()
18091810
content_type = response.headers.get("Content-Type", "")
18101811

18111812
if not content_type.startswith("image/"):
18121813
return drf.response.Response(
1813-
{"detail": "Invalid URL used."},
1814-
status=status.HTTP_400_BAD_REQUEST
1814+
{"detail": "Invalid URL used."}, status=status.HTTP_400_BAD_REQUEST
18151815
)
18161816

18171817
# Use StreamingHttpResponse with the response's iter_content to properly stream the data
@@ -1829,7 +1829,7 @@ def cors_proxy(self, request, *args, **kwargs):
18291829
except requests.RequestException as e:
18301830
logger.exception(e)
18311831
return drf.response.Response(
1832-
{"error": f"Failed to fetch resource from {url}"},
1832+
{"detail": "Invalid URL used."},
18331833
status=status.HTTP_400_BAD_REQUEST,
18341834
)
18351835

src/backend/core/tests/documents/test_api_documents_cors_proxy.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,53 @@ def test_api_docs_cors_proxy_unsupported_media_type(mock_getaddrinfo):
190190
assert response.json() == {"detail": "Invalid URL used."}
191191

192192

193+
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
194+
@responses.activate
195+
def test_api_docs_cors_proxy_redirect(mock_getaddrinfo):
196+
"""Test the CORS proxy API for documents with a redirect."""
197+
document = factories.DocumentFactory(link_reach="public")
198+
199+
# Mock DNS resolution to return a public IP address
200+
mock_getaddrinfo.return_value = [
201+
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
202+
]
203+
204+
client = APIClient()
205+
url_to_fetch = "https://external-url.com/assets/index.html"
206+
responses.get(
207+
url_to_fetch,
208+
body=b"",
209+
status=302,
210+
headers={"Location": "https://external-url.com/other/assets/index.html"},
211+
)
212+
response = client.get(
213+
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
214+
)
215+
assert response.status_code == 400
216+
assert response.json() == {"detail": "Invalid URL used."}
217+
218+
219+
@unittest.mock.patch("core.api.viewsets.socket.getaddrinfo")
220+
@responses.activate
221+
def test_api_docs_cors_proxy_url_not_returning_200(mock_getaddrinfo):
222+
"""Test the CORS proxy API for documents with a URL that does not return 200."""
223+
document = factories.DocumentFactory(link_reach="public")
224+
225+
# Mock DNS resolution to return a public IP address
226+
mock_getaddrinfo.return_value = [
227+
(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("8.8.8.8", 0))
228+
]
229+
230+
client = APIClient()
231+
url_to_fetch = "https://external-url.com/assets/index.html"
232+
responses.get(url_to_fetch, body=b"", status=404)
233+
response = client.get(
234+
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
235+
)
236+
assert response.status_code == 400
237+
assert response.json() == {"detail": "Invalid URL used."}
238+
239+
193240
@pytest.mark.parametrize(
194241
"url_to_fetch",
195242
[
@@ -229,9 +276,7 @@ def test_api_docs_cors_proxy_request_failed(mock_getaddrinfo):
229276
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
230277
)
231278
assert response.status_code == 400
232-
assert response.json() == {
233-
"error": "Failed to fetch resource from https://external-url.com/assets/index.html"
234-
}
279+
assert response.json() == {"detail": "Invalid URL used."}
235280

236281

237282
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)