This is what you get when we find something. Real finding from paperless-ngx, patched in v2.20.6.
A user who is granted change_document on a shared document can change the document's
owner field, effectively transferring ownership without the original owner's consent.
This allows privilege escalation (persisting access and potentially locking out the original owner)
and violates expected ownership controls.
DocumentSerializer exposes a writable owner field. When a user has
change_document on a shared document, DocumentViewSet permits the
update and the serializer does not restrict ownership changes to owners/admins.
Relevant code:
src/documents/serialisers.py — DocumentSerializer includes owner = serializers.PrimaryKeyRelatedField(...) (writable)src/documents/serialisers.py — OwnedObjectSerializer.update: no restriction on changing owner during updatesrc/documents/views.py — DocumentViewSet uses PaperlessObjectPermissions allowing updates for users with change_document permissionResult: any user with change_document can set owner to themselves or others.
Preconditions
change_document on that document (e.g., via sharing).Steps
User B updates the document's owner:
curl -X PATCH "http://localhost:8000/api/documents/<DOC_ID>/" \
-H "Cookie: csrftoken=CSRF_TOKEN; sessionid=SESSION_ID" \
-H "X-CSRFToken: CSRF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner": <USER_B_ID>}' 200 OK, and the document owner is now User B.Expected
The request should be rejected (403) unless the requester is the current owner or an admin.
This is an authorization/privilege escalation issue that allows a collaborator
with change_document to take ownership of documents. Consequences include:
We include a test you can drop into your suite to verify the fix and prevent regressions.
class TestDocumentPermissionHardening(
DirectoriesMixin,
DocumentConsumeDelayMixin,
APITestCase,
):
def test_document_owner_change_requires_owner_or_admin(self):
owner = User.objects.create_user(username="owner")
editor = User.objects.create_user(username="editor")
editor.user_permissions.add(
Permission.objects.get(codename="view_document"),
Permission.objects.get(codename="change_document"),
)
doc = Document.objects.create(
title="Owned doc",
content="sensitive",
checksum="abc123",
mime_type="application/pdf",
owner=owner,
)
assign_perm("view_document", editor, doc)
assign_perm("change_document", editor, doc)
self.client.force_authenticate(editor)
response = self.client.patch(
f"/api/documents/{doc.pk}/",
{"owner": editor.pk},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
doc.refresh_from_db()
self.assertEqual(doc.owner_id, owner.id) This is the quality bar for every finding we report. No noise — only validated issues with clear impact and fix guidance.