Sample finding

This is what you get when we find something. Real finding from paperless-ngx, patched in v2.20.6.

Medium severity

Document editors can alter permissions via the documents update API

paperless-ngx/paperless-ngx Python Django Django REST Framework

Summary

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.

Details

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.pyDocumentSerializer includes owner = serializers.PrimaryKeyRelatedField(...) (writable)
  • src/documents/serialisers.pyOwnedObjectSerializer.update: no restriction on changing owner during update
  • src/documents/views.pyDocumentViewSet uses PaperlessObjectPermissions allowing updates for users with change_document permission

Result: any user with change_document can set owner to themselves or others.

Proof of Concept

Preconditions

  • User A owns a document.
  • User B is granted change_document on that document (e.g., via sharing).

Steps

  1. 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>}'
  2. Observe 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.

Impact

This is an authorization/privilege escalation issue that allows a collaborator with change_document to take ownership of documents. Consequences include:

  • Persistent access beyond intended sharing
  • Potential lockout of the original owner
  • Unauthorized reassignment of document ownership

Integration test

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)

Classification

CWE-862 Missing Authorization
CWE-863 Incorrect Authorization
CWE-269 Improper Privilege Management
GHSA-w47q-3m69-84v8 — View advisory on GitHub

This is the quality bar for every finding we report. No noise — only validated issues with clear impact and fix guidance.