Nautobot allows unauthenticated db-file-storage views
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Nautobot file download/view URLs lack authentication, allowing unauthenticated access to uploaded job files if the filename is known.
Vulnerability
CVE-2023-50263 is an authentication bypass vulnerability in Nautobot, a Network Source of Truth and Network Automation Platform. The issue affects Nautobot versions 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 uploaded as part of a Job run request that includes FileVar inputs. In the default implementation using django-db-file-storage, these URLs do not require any user authentication, contrary to the intended design that would restrict access to users with permissions on the FileProxy model instances [1].
Exploitation
To exploit this vulnerability, an attacker must know the exact file name/path value of a target file. The application does not provide any mechanism for listing or enumerating available file names, so exploitation requires guessing or prior knowledge of a specific file name. No authentication is needed to access the file if the name is known [1]. The files are ephemeral and normally deleted after the job runs, but an attacker who identifies a valid name before deletion can download it [1].
Impact
An unauthenticated attacker who knows a valid file name can download or view files uploaded as part of job run requests. These files may contain sensitive data from network automation tasks, such as configuration files, credentials, or other operational data. The vulnerability is classified with a CVSS score and affects the confidentiality of the system [1].
Mitigation
Fixes are included in Nautobot versions 1.6.7 and 2.0.6. The patches add authentication and permission checks to the DB file storage views [2][3][4]. No workarounds are available; users must upgrade to the patched versions [1].
- NVD - CVE-2023-50263
- [1.6] Require authentication on DB file storage views by glennmatthews · Pull Request #4964 · nautobot/nautobot
- requires authentication on DB file storage views by Kircheneer · Pull Request #4959 · nautobot/nautobot
- [1.6] Require authentication on DB file storage views (#4964) · nautobot/nautobot@7c4cf31
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nautobotPyPI | >= 1.1.0, < 1.6.7 | 1.6.7 |
nautobotPyPI | >= 2.0.0, < 2.0.6 | 2.0.6 |
Affected products
2- nautobot/nautobotv5Range: >= 1.1.0, < 1.6.7
Patches
37c4cf3137f45[1.6] Require authentication on DB file storage views (#4964)
4 files changed · +98 −5
changes/4959.security+1 −0 added@@ -0,0 +1 @@ +Enforce authentication and object permissions on DB file storage views (https://github.com/nautobot/nautobot/security/advisories/GHSA-75mc-3pjc-727q).
nautobot/core/tests/test_views.py+61 −0 modified@@ -2,12 +2,17 @@ from unittest import mock import urllib.parse +from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import override_settings from django.test.utils import override_script_prefix from django.urls import get_script_prefix, reverse from prometheus_client.parser import text_string_to_metric_families +from nautobot.extras.models import FileProxy from nautobot.extras.registry import registry +from nautobot.users.models import ObjectPermission +from nautobot.utilities.permissions import get_permission_for_model from nautobot.utilities.testing import TestCase @@ -408,3 +413,59 @@ def test_500_custom_support_message(self, mock_get): self.assertNotContains(response, "Network to Code", status_code=500) response_content = response.content.decode(response.charset) self.assertInHTML("Hello world!", response_content) + + +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)
nautobot/core/urls.py+21 −3 modified@@ -1,9 +1,16 @@ from django.conf import settings -from django.conf.urls import include +from django.conf.urls import include, url from django.urls import path from django.views.static import serve -from nautobot.core.views import CustomGraphQLView, HomeView, StaticMediaFailureView, SearchView, nautobot_metrics_view +from nautobot.core.views import ( + CustomGraphQLView, + HomeView, + StaticMediaFailureView, + SearchView, + nautobot_metrics_view, + get_file_with_authorization, +) from nautobot.extras.plugins.urls import ( plugin_admin_patterns, plugin_patterns, @@ -46,7 +53,18 @@ # django-health-check path(r"health/", include("health_check.urls")), # FileProxy attachments download/get URLs used in admin views only - path("files/", include("db_file_storage.urls")), + 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", + ), ]
nautobot/core/views/__init__.py+15 −2 modified@@ -3,11 +3,13 @@ import sys import time +from db_file_storage.views import get_file import prometheus_client from django.conf import settings +from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import AccessMixin from django.http import HttpResponseServerError, JsonResponse, HttpResponseForbidden, HttpResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, RequestContext, Template from django.template.exceptions import TemplateDoesNotExist from django.urls import reverse @@ -24,10 +26,11 @@ from nautobot.core.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES from nautobot.core.forms import SearchForm from nautobot.core.releases import get_latest_release -from nautobot.extras.models import GraphQLQuery +from nautobot.extras.models import GraphQLQuery, FileProxy from nautobot.extras.registry import registry from nautobot.extras.forms import GraphQLQueryForm from nautobot.utilities.config import get_settings_or_config +from nautobot.utilities.permissions import get_permission_for_model class HomeView(AccessMixin, TemplateView): @@ -278,3 +281,13 @@ def nautobot_metrics_view(request): pass metrics_page = prometheus_client.generate_latest(prometheus_registry) return HttpResponse(metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST) + + +@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)
458280c359a4requires authentication on DB file storage views (#4959)
4 files changed · +97 −5
changes/4959.security+1 −0 added@@ -0,0 +1 @@ +Enforce authentication and object permissions on DB file storage views (https://github.com/nautobot/nautobot/security/advisories/GHSA-75mc-3pjc-727q).
nautobot/core/tests/test_views.py+60 −0 modified@@ -3,17 +3,21 @@ import urllib.parse from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import RequestFactory, override_settings from django.test.utils import override_script_prefix from django.urls import get_script_prefix, reverse from prometheus_client.parser import text_string_to_metric_families from nautobot.core.testing import TestCase +from nautobot.core.utils.permissions import get_permission_for_model from nautobot.core.views.mixins import GetReturnURLMixin from nautobot.dcim.models.locations import Location from nautobot.extras.choices import CustomFieldTypeChoices +from nautobot.extras.models import FileProxy from nautobot.extras.models.customfields import CustomField, CustomFieldChoice from nautobot.extras.registry import registry +from nautobot.users.models import ObjectPermission class GetReturnURLMixinTestCase(TestCase): @@ -476,3 +480,59 @@ def test_500_custom_support_message(self, mock_get): self.assertNotContains(response, "Network to Code", status_code=500) response_content = response.content.decode(response.charset) self.assertInHTML("Hello world!", response_content) + + +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)
nautobot/core/urls.py+21 −3 modified@@ -1,9 +1,16 @@ from django.conf import settings -from django.conf.urls import include +from django.conf.urls import include, url from django.urls import path from django.views.static import serve -from nautobot.core.views import CustomGraphQLView, HomeView, StaticMediaFailureView, SearchView, nautobot_metrics_view +from nautobot.core.views import ( + CustomGraphQLView, + HomeView, + StaticMediaFailureView, + SearchView, + nautobot_metrics_view, + get_file_with_authorization, +) from nautobot.extras.plugins.urls import ( plugin_admin_patterns, plugin_patterns, @@ -45,7 +52,18 @@ # django-health-check path(r"health/", include("health_check.urls")), # FileProxy attachments download/get URLs used in admin views only - path("files/", include("db_file_storage.urls")), + 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", + ), ]
nautobot/core/views/__init__.py+15 −2 modified@@ -3,12 +3,14 @@ import sys import time +from db_file_storage.views import get_file from django.apps import apps import prometheus_client from django.conf import settings +from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import AccessMixin from django.http import HttpResponseServerError, JsonResponse, HttpResponseForbidden, HttpResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.template import loader, RequestContext, Template from django.template.exceptions import TemplateDoesNotExist from django.urls import resolve, reverse @@ -27,7 +29,8 @@ from nautobot.core.releases import get_latest_release from nautobot.core.utils.config import get_settings_or_config from nautobot.core.utils.lookup import get_route_for_model -from nautobot.extras.models import GraphQLQuery +from nautobot.core.utils.permissions import get_permission_for_model +from nautobot.extras.models import GraphQLQuery, FileProxy from nautobot.extras.registry import registry from nautobot.extras.forms import GraphQLQueryForm @@ -294,3 +297,13 @@ def nautobot_metrics_view(request): pass metrics_page = prometheus_client.generate_latest(prometheus_registry) return HttpResponse(metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST) + + +@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)
5e2ba9e8ac08requires authentication on DB file storage views
4 files changed · +62 −5
changes/4959.security+1 −0 added@@ -0,0 +1 @@ +Require authentication on DB file storage views (https://github.com/nautobot/nautobot/security/advisories/GHSA-75mc-3pjc-727q). \ No newline at end of file
nautobot/core/tests/test_views.py+30 −1 modified@@ -2,13 +2,16 @@ from unittest import mock import urllib.parse +from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import override_settings from django.test.utils import override_script_prefix from django.urls import get_script_prefix, reverse from prometheus_client.parser import text_string_to_metric_families from nautobot.extras.registry import registry -from nautobot.utilities.testing import TestCase +from nautobot.utilities.permissions import get_permission_for_model +from nautobot.utilities.testing import TestCase, User class HomeViewTestCase(TestCase): @@ -408,3 +411,29 @@ def test_500_custom_support_message(self, mock_get): self.assertNotContains(response, "Network to Code", status_code=500) response_content = response.content.decode(response.charset) self.assertInHTML("Hello world!", response_content) + + +class DBFileStorageViewTestCase(TestCase): + """Test overwritten views for django_db_file_storage views""" + + def setUp(self): + self.user = User.objects.create(username="testuser") + self.test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n") + self.file_proxy = FileProxy.objects.create(name=self.test_file.name, file=self.test_file) + self.urls = [ + f"{reverse('db_file_storage.download_file')}?name={self.file_proxy.file.name}", + f"{reverse('db_file_storage.get_file')}?name={self.file_proxy.file.name}", + ] + + def test_authorization_failed(self): + for url in self.urls: + with self.subTest(url): + response = self.client.get(url) + self.assertHttpStatus(response, 403) + + def test_authorization_succeeded(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)
nautobot/core/urls.py+21 −3 modified@@ -1,9 +1,16 @@ from django.conf import settings -from django.conf.urls import include +from django.conf.urls import include, url from django.urls import path from django.views.static import serve -from nautobot.core.views import CustomGraphQLView, HomeView, StaticMediaFailureView, SearchView, nautobot_metrics_view +from nautobot.core.views import ( + CustomGraphQLView, + HomeView, + StaticMediaFailureView, + SearchView, + nautobot_metrics_view, + get_file_with_authorization, +) from nautobot.extras.plugins.urls import ( plugin_admin_patterns, plugin_patterns, @@ -46,7 +53,18 @@ # django-health-check path(r"health/", include("health_check.urls")), # FileProxy attachments download/get URLs used in admin views only - path("files/", include("db_file_storage.urls")), + 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", + ), ]
nautobot/core/views/__init__.py+10 −1 modified@@ -3,8 +3,10 @@ import sys import time +from db_file_storage.views import get_file import prometheus_client from django.conf import settings +from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import AccessMixin from django.http import HttpResponseServerError, JsonResponse, HttpResponseForbidden, HttpResponse from django.shortcuts import redirect, render @@ -24,10 +26,11 @@ from nautobot.core.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES from nautobot.core.forms import SearchForm from nautobot.core.releases import get_latest_release -from nautobot.extras.models import GraphQLQuery +from nautobot.extras.models import GraphQLQuery, FileProxy from nautobot.extras.registry import registry from nautobot.extras.forms import GraphQLQueryForm from nautobot.utilities.config import get_settings_or_config +from nautobot.utilities.permissions import get_permission_for_model class HomeView(AccessMixin, TemplateView): @@ -278,3 +281,9 @@ def nautobot_metrics_view(request): pass metrics_page = prometheus_client.generate_latest(prometheus_registry) return HttpResponse(metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST) + + +@permission_required(get_permission_for_model(FileProxy, "view"), raise_exception=True) +def get_file_with_authorization(*args, **kwargs): + """Patch db_file_storage view with authentication.""" + return get_file(*args, **kwargs)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-75mc-3pjc-727qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-50263ghsaADVISORY
- github.com/nautobot/nautobot/commit/458280c359a4833a20da294eaf4b8d55edc91ceeghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/commit/5e2ba9e8ac0840b1c44eb1a8ea3c0bd2c68e4f80ghsaWEB
- github.com/nautobot/nautobot/commit/7c4cf3137f45f1541f09f2f6a7f8850cd3a2eaeeghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/4959ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/4964ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/security/advisories/GHSA-75mc-3pjc-727qghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/nautobot/PYSEC-2023-286.yamlghsaWEB
- github.com/victor-o-silva/db_file_storage/blob/master/db_file_storage/views.pyghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.