🐛(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:
@@ -8,8 +8,14 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
- 📝(doc) add publiccode.yml
|
- 📝(doc) add publiccode.yml
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- 🐛(back) allow only images to be used with the cors-proxy #781
|
||||||
|
|
||||||
## [2.5.0] - 2025-03-18
|
## [2.5.0] - 2025-03-18
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|||||||
@@ -1271,13 +1271,21 @@ class DocumentViewSet(
|
|||||||
},
|
},
|
||||||
timeout=10,
|
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
|
# Use StreamingHttpResponse with the response's iter_content to properly stream the data
|
||||||
proxy_response = StreamingHttpResponse(
|
proxy_response = StreamingHttpResponse(
|
||||||
streaming_content=response.iter_content(chunk_size=8192),
|
streaming_content=response.iter_content(chunk_size=8192),
|
||||||
content_type=response.headers.get(
|
content_type=content_type,
|
||||||
"Content-Type", "application/octet-stream"
|
headers={
|
||||||
),
|
"Content-Disposition": "attachment;",
|
||||||
|
"Content-Security-Policy": "default-src 'none'; img-src 'none' data:;",
|
||||||
|
},
|
||||||
status=response.status_code,
|
status=response.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Test on the CORS proxy API for documents."""
|
"""Test on the CORS proxy API for documents."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import responses
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories
|
from core import factories
|
||||||
@@ -8,17 +9,24 @@ from core import factories
|
|||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
def test_api_docs_cors_proxy_valid_url():
|
def test_api_docs_cors_proxy_valid_url():
|
||||||
"""Test the CORS proxy API for documents with a valid URL."""
|
"""Test the CORS proxy API for documents with a valid URL."""
|
||||||
document = factories.DocumentFactory(link_reach="public")
|
document = factories.DocumentFactory(link_reach="public")
|
||||||
|
|
||||||
client = APIClient()
|
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(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["Content-Type"] == "image/png"
|
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
|
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"}
|
assert response.json() == {"detail": "Missing 'url' query parameter"}
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
def test_api_docs_cors_proxy_anonymous_document_not_public():
|
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."""
|
"""Test the CORS proxy API for documents with an anonymous user and a non-public document."""
|
||||||
document = factories.DocumentFactory(link_reach="authenticated")
|
document = factories.DocumentFactory(link_reach="authenticated")
|
||||||
|
|
||||||
client = APIClient()
|
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(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
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():
|
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
|
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 = APIClient()
|
||||||
client.force_login(user)
|
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(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["Content-Type"] == "image/png"
|
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
|
assert response.streaming_content
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
|
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
|
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 = APIClient()
|
||||||
client.force_login(user)
|
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(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
|
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() == {
|
assert response.json() == {
|
||||||
"detail": "You do not have permission to perform this action."
|
"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
|
||||||
|
|||||||
Reference in New Issue
Block a user