🔒️(backend) scan files written through WOPI

PutFile wrote the content straight to object storage, while malware
detection only ran on the upload endpoint. Editing a document was
therefore a way to store content that was never analysed.

The item stays READY during the analysis: a collaborator cannot open a
file that is not READY, so flipping the state would eject everyone from
the document on every save.
This commit is contained in:
Nicolas Clerc
2026-08-07 12:39:16 +02:00
parent 5a4b36c942
commit 9ec728847d
3 changed files with 23 additions and 10 deletions
+1
View File
@@ -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
@@ -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,6 +41,7 @@ def test_put_file_content_connected_user_with_access():
client = APIClient()
assert item.size == 0
updated_at = item.updated_at
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",
@@ -48,6 +52,8 @@ def test_put_file_content_connected_user_with_access():
"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
+5
View File
@@ -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,