CVE-2023-50263 (Python, published 2023-12-12T23:15Z)
Description: value: Nautobot is a Network Source of Truth and Network Automation Platform built as a web application atop the Django Python framework with a PostgreSQL or MySQL database. In Nautobot 1.x and 2.0.x prior to 1.6.7 and 2.0.6, the URLs `/files/get/?name=...` and `/files/download/?name=...` are used to provide admin access to files that have been uploaded as part of a run request for a Job that has FileVar inputs. Under normal operation these files are ephemeral and are deleted once the Job in question runs. 

In the default implementation used in Nautobot, as provided by `django-db-file-storage`, these URLs do not by default require any user authentication to access; they should instead be restricted to only users who have permissions to view Nautobot's `FileProxy` model instances.

Note that no URL mechanism is provided for listing or traversal of the available file `name` values, so in practice an unauthenticated user would have to guess names to discover arbitrary files for download, but if a user knows the file name/path value, they can access it without authenticating, so we are considering this a vulnerability.

Fixes are included in Nautobot 1.6.7 and Nautobot 2.0.6. No known workarounds are available other than applying the patches included in those versions.
Severity: nan (CVSS v3 base score: 5.3)
Weakness: CWE-306 \u2014 Missing Authentication for Critical Function
The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.
Repository: https://github.com/nautobot/nautobot
Files changed:
changes/4959.security
nautobot/core/tests/test_views.py
nautobot/core/urls.py
nautobot/core/views/__init__.py
Commit message: requires authentication on DB file storage views (#4959)

* requires authentication on DB file storage views

* Update change fragment, add object permissions enforcement

---------

Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com>

Vulnerable code:
from django.conf.urls import include
from nautobot.core.views import CustomGraphQLView, HomeView, StaticMediaFailureView, SearchView, nautobot_metrics_view
    path("files/", include("db_file_storage.urls")),
from django.shortcuts import redirect, render
from nautobot.extras.models import GraphQLQuery

Fixed code:
Enforce authentication and object permissions on DB file storage views (https://github.com/nautobot/nautobot/security/advisories/GHSA-75mc-3pjc-727q).
from django.core.files.uploadedfile import SimpleUploadedFile
from nautobot.core.utils.permissions import get_permission_for_model
from nautobot.extras.models import FileProxy
from nautobot.users.models import ObjectPermission


class DBFileStorageViewTestCase(TestCase):
    """Test authentication/permission enforcement for django_db_file_storage views."""

    def setUp(self):
        super().setUp()
        self.test_file_1 = SimpleUploadedFile(name="test_file_1.txt", content=b"I am content.\n")
        self.file_proxy_1 = FileProxy.objects.create(name=self.test_file_1.name, file=self.test_file_1)
        self.test_file_2 = SimpleUploadedFile(name="test_file_2.txt", content=b"I am content.\n")
        self.file_proxy_2 = FileProxy.objects.create(name=self.test_file_2.name, file=self.test_file_2)
        self.urls = [
            f"{reverse('db_file_storage.download_file')}?name={self.file_proxy_1.file.name}",
            f"{reverse('db_file_storage.get_file')}?name={self.file_proxy_1.file.name}",
        ]

    def test_get_file_anonymous(self):
        self.client.logout()
        for url in self.urls:
            with self.subTest(url):
                response = self.client.get(url)
                self.assertHttpStatus(response, 403)

    def test_get_file_without_permission(self):
        for url in self.urls:
            with self.subTest(url):
                response = self.client.get(url)
                self.assertHttpStatus(response, 403)

    def test_get_object_with_permission(self):
        self.add_permissions(get_permission_for_model(FileProxy, "view"))
        for url in self.urls:
            with self.subTest(url):
                response = self.client.get(url)
                self.assertHttpStatus(response, 200)

    def test_get_object_with_constrained_permission(self):
        obj_perm = ObjectPermission(
            name="Test permission",
            constraints={"pk": self.file_proxy_1.pk},
            actions=["view"],
        )
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ContentType.objects.get_for_model(FileProxy))
        for url in self.urls:
            with self.subTest(url):
                response = self.client.get(url)
                self.assertHttpStatus(response, 200)
        for url in [
            f"{reverse('db_file_storage.download_file')}?name={self.file_proxy_2.file.name}",
            f"{reverse('db_file_storage.get_file')}?name={self.file_proxy_2.file.name}",
        ]:
            with self.subTest(url):
                response = self.client.get(url)
                self.assertHttpStatus(response, 404)
from django.conf.urls import include, url
from nautobot.core.views import (
    CustomGraphQLView,
    HomeView,
    StaticMediaFailureView,
    SearchView,
    nautobot_metrics_view,
    get_file_with_authorization,
)
    url(
        "files/download/",
        get_file_with_authorization,
        {"add_attachment_headers": True},
        name="db_file_storage.download_file",
    ),
    url(
        "files/get/",
        get_file_with_authorization,
        {"add_attachment_headers": False},
        name="db_file_storage.get_file",
    ),
from db_file_storage.views import get_file
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect, render
from nautobot.core.utils.permissions import get_permission_for_model
from nautobot.extras.models import GraphQLQuery, FileProxy


@permission_required(get_permission_for_model(FileProxy, "view"), raise_exception=True)
def get_file_with_authorization(request, *args, **kwargs):
    """Patch db_file_storage view with authentication."""
    # Make sure user has permissions
    queryset = FileProxy.objects.restrict(request.user, "view")
    get_object_or_404(queryset, file=request.GET.get("name"))

    return get_file(request, *args, **kwargs)