diff --git a/CHANGELOG.md b/CHANGELOG.md index aaeafa76..66d80cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to - 🐛(docker) pin collabora image and adapt to its new runtime contract - 🐛(backend) delete malware detection record when purging an item - 🔒️(backend) reject unsafe filenames requested by WOPI renames +- 🔒️(backend) analyze file content written through WOPI ## [v0.20.0] - 2026-07-15 diff --git a/src/backend/wopi/tests/viewset/test_put_file_content.py b/src/backend/wopi/tests/viewset/test_put_file_content.py index 91db54a8..3a95d8a2 100644 --- a/src/backend/wopi/tests/viewset/test_put_file_content.py +++ b/src/backend/wopi/tests/viewset/test_put_file_content.py @@ -1,8 +1,11 @@ """Test the PUT file content viewset.""" +from unittest import mock + from django.core.files.storage import default_storage import pytest +from lasuite.malware_detection import malware_detection from rest_framework.test import APIClient from core import factories, models @@ -38,16 +41,19 @@ def test_put_file_content_connected_user_with_access(): client = APIClient() assert item.size == 0 updated_at = item.updated_at - response = client.post( - f"/api/v1.0/wopi/files/{item.id}/contents/", - data=b"new content", - content_type="text/plain", - HTTP_AUTHORIZATION=f"Bearer {access_token}", - headers={ - "X-WOPI-Override": "PUT", - "X-WOPI-Lock": "1234567890", - }, - ) + with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file: + response = client.post( + f"/api/v1.0/wopi/files/{item.id}/contents/", + data=b"new content", + content_type="text/plain", + HTTP_AUTHORIZATION=f"Bearer {access_token}", + headers={ + "X-WOPI-Override": "PUT", + "X-WOPI-Lock": "1234567890", + }, + ) + + mock_analyse_file.assert_called_once_with(item.file_key, item_id=item.id) assert response.status_code == 200 assert "X-WOPI-ItemVersion" in response.headers @@ -61,6 +67,7 @@ def test_put_file_content_connected_user_with_access(): assert response.headers.get("X-WOPI-ItemVersion") == file["ETag"].strip('"') item.refresh_from_db() assert item.size == 11 # the size should have been updated + assert item.upload_state == models.ItemUploadStateChoices.READY assert item.updated_at > updated_at diff --git a/src/backend/wopi/viewsets.py b/src/backend/wopi/viewsets.py index 15172774..aba32f88 100644 --- a/src/backend/wopi/viewsets.py +++ b/src/backend/wopi/viewsets.py @@ -11,6 +11,7 @@ from django.core.files.storage import default_storage from django.db import transaction from django.http import StreamingHttpResponse +from lasuite.malware_detection import malware_detection from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response @@ -187,8 +188,12 @@ class WopiViewSet(viewsets.ViewSet): s3_client = default_storage.connection.meta.client default_storage.save(item.file_key, file) item.size = file.size + # Keep the item READY during re-analysis: non-creators cannot open + # non-READY files in WOPI. item.save(update_fields=["size", "updated_at"]) + malware_detection.analyse_file(item.file_key, item_id=item.id) + head_response = s3_client.head_object(Bucket=default_storage.bucket_name, Key=item.file_key) return Response( status=200,