🐛(back) allow only images to be used with the cors-proxy

The cors-proxy endpoint allowed to use every type of files and to
execute it in the browser. We limit the scope only to images and
Content-Security-Policy and Content-Disposition headers are also added
to not allow script execution that can be present in a SVG file.
This commit is contained in:
Manuel Raynaud
2025-03-20 11:04:02 +01:00
parent 3a6105cc7e
commit 313acf4f78
3 changed files with 54 additions and 7 deletions

View File

@@ -8,8 +8,14 @@ and this project adheres to
## [Unreleased]
## Added
- 📝(doc) add publiccode.yml
## Fixed
- 🐛(back) allow only images to be used with the cors-proxy #781
## [2.5.0] - 2025-03-18
## Added

View File

@@ -1271,13 +1271,21 @@ class DocumentViewSet(
},
timeout=10,
)
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
return drf.response.Response(
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
)
# Use StreamingHttpResponse with the response's iter_content to properly stream the data
proxy_response = StreamingHttpResponse(
streaming_content=response.iter_content(chunk_size=8192),
content_type=response.headers.get(
"Content-Type", "application/octet-stream"
),
content_type=content_type,
headers={
"Content-Disposition": "attachment;",
"Content-Security-Policy": "default-src 'none'; img-src 'none' data:;",
},
status=response.status_code,
)

View File

@@ -1,6 +1,7 @@
"""Test on the CORS proxy API for documents."""
import pytest
import responses
from rest_framework.test import APIClient
from core import factories
@@ -8,17 +9,24 @@ from core import factories
pytestmark = pytest.mark.django_db
@responses.activate
def test_api_docs_cors_proxy_valid_url():
"""Test the CORS proxy API for documents with a valid URL."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
assert response.streaming_content
@@ -32,12 +40,14 @@ def test_api_docs_cors_proxy_without_url_query_string():
assert response.json() == {"detail": "Missing 'url' query parameter"}
@responses.activate
def test_api_docs_cors_proxy_anonymous_document_not_public():
"""Test the CORS proxy API for documents with an anonymous user and a non-public document."""
document = factories.DocumentFactory(link_reach="authenticated")
client = APIClient()
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
@@ -47,6 +57,7 @@ def test_api_docs_cors_proxy_anonymous_document_not_public():
}
@responses.activate
def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
"""
Test the CORS proxy API for documents with an authenticated user accessing a protected
@@ -58,15 +69,22 @@ def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
client = APIClient()
client.force_login(user)
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
assert response.streaming_content
@responses.activate
def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
"""
Test the CORS proxy API for documents with an authenticated user not accessing a restricted
@@ -78,7 +96,8 @@ def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
client = APIClient()
client.force_login(user)
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
@@ -86,3 +105,17 @@ def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@responses.activate
def test_api_docs_cors_proxy_unsupported_media_type():
"""Test the CORS proxy API for documents with an unsupported media type."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://external-url.com/assets/index.html"
responses.get(url_to_fetch, body=b"", status=200, content_type="text/html")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 415