Nautobot may allows uploaded media files to be accessible without authentication
Description
Nautobot is a Network Source of Truth and Network Automation Platform. Prior to v2.4.10 and v1.6.32 , files uploaded by users to Nautobot's MEDIA_ROOT directory, including DeviceType image attachments as well as images attached to a Location, Device, or Rack, are served to users via a URL endpoint that was not enforcing user authentication. As a consequence, such files can be retrieved by anonymous users who know or can guess the correct URL for a given file. Nautobot v2.4.10 and v1.6.32 address this issue by adding enforcement of Nautobot user authentication to this endpoint.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Nautobot prior to v2.4.10 and v1.6.32 serves user-uploaded media files without authentication, allowing anonymous access to potentially sensitive files.
Vulnerability
Overview
CVE-2025-49143 is an authentication bypass vulnerability in Nautobot, a Network Source of Truth and Network Automation Platform. Prior to versions v2.4.10 and v1.6.32, the URL endpoint that serves files uploaded to the MEDIA_ROOT directory did not enforce user authentication [1]. This includes DeviceType image attachments and images attached to Locations, Devices, or Racks, which could be retrieved by any anonymous user who knows or guesses the correct file URL [1].
Exploitation
The attack vector is network-based and requires no authentication or user interaction. An attacker only needs to discover or guess the URL path to a specific media file. Nautobot uses predictable file storage patterns; once a file is uploaded, it is accessible via a known endpoint. The vulnerability was addressed in pull request #6703, which added authentication checks to the media view [2].
Impact
A successful exploit allows an anonymous user to view or download files that were intended to be restricted. Depending on the uploaded content, this could expose organizational network diagrams, device images, or other sensitive data stored in media attachments. There is no impact on integrity or availability, but confidentiality is directly compromised [1].
Mitigation
The issue is fixed in Nautobot versions v2.4.10 and v1.6.32 [1]. Administrators are advised to upgrade immediately. The fix enforces that users must be authenticated to access the media endpoint, except for files specifically designated as branding assets in the BRANDING_FILEPATHS setting, which are intentionally public [4]. No workaround is provided for older versions.
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.6.32 | 1.6.32 |
nautobotPyPI | >= 2.0.0, < 2.4.10 | 2.4.10 |
Affected products
3- nautobot/nautobotv5Range: < 1.6.32
Patches
2d99a53b06512[LTM] require authentication for media files (#6703)
4 files changed · +98 −3
changes/6672.security+1 −0 added@@ -0,0 +1 @@ +Added enforcement of user authentication when serving uploaded media files (GHSA-rh67-4c8j-hjjh).
nautobot/core/tests/test_views.py+73 −0 modified@@ -1,4 +1,6 @@ +import os import re +import tempfile from unittest import mock import urllib.parse @@ -133,6 +135,77 @@ def test_banners_no_xss(self): self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset)) +class MediaViewTestCase(TestCase): + def test_media_unauthenticated(self): + """ + Test that unauthenticated users are redirected to login when accessing media files whether they exist or not. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings( + MEDIA_ROOT=temp_dir, + BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")}, + ): + file_path = os.path.join(temp_dir, "foo.txt") + url = reverse("media", kwargs={"path": "foo.txt"}) + self.client.logout() + + # Unauthenticated request to nonexistent media file should redirect to login page + response = self.client.get(url) + self.assertRedirects( + response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200 + ) + + # Unauthenticated request to existent media file should redirect to login page as well + with open(file_path, "w") as f: + f.write("Hello, world!") + response = self.client.get(url) + self.assertRedirects( + response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200 + ) + + def test_branding_media(self): + """ + Test that users can access branding files listed in `settings.BRANDING_FILEPATHS` regardless of authentication. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings( + MEDIA_ROOT=temp_dir, + BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")}, + ): + os.makedirs(os.path.join(temp_dir, "branding")) + file_path = os.path.join(temp_dir, "branding", "logo.txt") + with open(file_path, "w") as f: + f.write("Hello, world!") + + url = reverse("media", kwargs={"path": "branding/logo.txt"}) + + # Authenticated request succeeds + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + # Unauthenticated request also succeeds + self.client.logout() + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + def test_media_authenticated(self): + """ + Test that authenticated users can access regular media files stored in the `MEDIA_ROOT`. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings(MEDIA_ROOT=temp_dir): + file_path = os.path.join(temp_dir, "foo.txt") + with open(file_path, "w") as f: + f.write("Hello, world!") + + url = reverse("media", kwargs={"path": "foo.txt"}) + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + @override_settings(BRANDING_TITLE="Nautobot") class SearchFieldsTestCase(TestCase): def test_search_bar_redirect_to_login(self):
nautobot/core/urls.py+3 −3 modified@@ -1,11 +1,11 @@ from django.conf import settings 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, + MediaView, StaticMediaFailureView, SearchView, nautobot_metrics_view, @@ -38,8 +38,8 @@ path("api/", include("nautobot.core.api.urls")), # GraphQL path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"), - # Serving static media in Django - path("media/<path:path>", serve, {"document_root": settings.MEDIA_ROOT}), + # Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use") + path("media/<path:path>", MediaView.as_view(), name="media"), # Admin path("admin/", admin_site.urls), path("admin/background-tasks/", include("django_rq.urls")),
nautobot/core/views/__init__.py+21 −0 modified@@ -1,5 +1,6 @@ import os import platform +import posixpath import sys import time @@ -17,6 +18,7 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.csrf import csrf_failure as _csrf_failure from django.views.generic import TemplateView, View +from django.views.static import serve from packaging import version from graphene_django.views import GraphQLView from prometheus_client import multiprocess @@ -110,6 +112,25 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) +class MediaView(AccessMixin, View): + """ + Serves media files while enforcing login restrictions. + + This view wraps Django's `serve()` function to ensure that access to media files (with the exception of + branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users. + """ + + def get(self, request, path): + if request.user.is_authenticated: + return serve(request, path, document_root=settings.MEDIA_ROOT) + + # Unauthenticated users can access BRANDING_FILEPATHS only + if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values(): + return serve(request, path, document_root=settings.MEDIA_ROOT) + + return self.handle_no_permission() + + class SearchView(AccessMixin, View): def get(self, request): # if user is not authenticated, redirect to login page
9c892dc30042Requesting media files requires authentication (#6672)
4 files changed · +97 −2
changes/6672.security+1 −0 added@@ -0,0 +1 @@ +Added enforcement of user authentication when serving uploaded media files (GHSA-rh67-4c8j-hjjh).
nautobot/core/tests/test_views.py+73 −0 modified@@ -1,5 +1,7 @@ import json +import os import re +import tempfile from unittest import mock, skipIf import urllib.parse @@ -185,6 +187,77 @@ def test_banners_no_xss(self): self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset)) +class MediaViewTestCase(TestCase): + def test_media_unauthenticated(self): + """ + Test that unauthenticated users are redirected to login when accessing media files whether they exist or not. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings( + MEDIA_ROOT=temp_dir, + BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")}, + ): + file_path = os.path.join(temp_dir, "foo.txt") + url = reverse("media", kwargs={"path": "foo.txt"}) + self.client.logout() + + # Unauthenticated request to nonexistent media file should redirect to login page + response = self.client.get(url) + self.assertRedirects( + response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200 + ) + + # Unauthenticated request to existent media file should redirect to login page as well + with open(file_path, "w") as f: + f.write("Hello, world!") + response = self.client.get(url) + self.assertRedirects( + response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200 + ) + + def test_branding_media(self): + """ + Test that users can access branding files listed in `settings.BRANDING_FILEPATHS` regardless of authentication. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings( + MEDIA_ROOT=temp_dir, + BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")}, + ): + os.makedirs(os.path.join(temp_dir, "branding")) + file_path = os.path.join(temp_dir, "branding", "logo.txt") + with open(file_path, "w") as f: + f.write("Hello, world!") + + url = reverse("media", kwargs={"path": "branding/logo.txt"}) + + # Authenticated request succeeds + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + # Unauthenticated request also succeeds + self.client.logout() + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + def test_media_authenticated(self): + """ + Test that authenticated users can access regular media files stored in the `MEDIA_ROOT`. + """ + with tempfile.TemporaryDirectory() as temp_dir: + with override_settings(MEDIA_ROOT=temp_dir): + file_path = os.path.join(temp_dir, "foo.txt") + with open(file_path, "w") as f: + f.write("Hello, world!") + + url = reverse("media", kwargs={"path": "foo.txt"}) + response = self.client.get(url) + self.assertHttpStatus(response, 200) + self.assertIn("Hello, world!", b"".join(response).decode(response.charset)) + + @override_settings(BRANDING_TITLE="Nautobot") class SearchFieldsTestCase(TestCase): def test_search_bar_redirect_to_login(self):
nautobot/core/urls.py+2 −2 modified@@ -2,13 +2,13 @@ from django.http import HttpResponse, HttpResponseNotFound from django.urls import include, path from django.views.generic import TemplateView -from django.views.static import serve from nautobot.core.views import ( AboutView, CustomGraphQLView, get_file_with_authorization, HomeView, + MediaView, NautobotMetricsView, NautobotMetricsViewAuth, RenderJinjaView, @@ -51,7 +51,7 @@ # GraphQL path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"), # Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use") - path("media/<path:path>", serve, {"document_root": settings.MEDIA_ROOT}), + path("media/<path:path>", MediaView.as_view(), name="media"), # Admin path("admin/", admin_site.urls), # Errors
nautobot/core/views/__init__.py+21 −0 modified@@ -3,6 +3,7 @@ import logging import os import platform +import posixpath import re import sys import time @@ -24,6 +25,7 @@ from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import TemplateView, View +from django.views.static import serve from graphene_django.views import GraphQLView from packaging import version from prometheus_client import ( @@ -133,6 +135,25 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) +class MediaView(AccessMixin, View): + """ + Serves media files while enforcing login restrictions. + + This view wraps Django's `serve()` function to ensure that access to media files (with the exception of + branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users. + """ + + def get(self, request, path): + if request.user.is_authenticated: + return serve(request, path, document_root=settings.MEDIA_ROOT) + + # Unauthenticated users can access BRANDING_FILEPATHS only + if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values(): + return serve(request, path, document_root=settings.MEDIA_ROOT) + + return self.handle_no_permission() + + class WorkerStatusView(UserPassesTestMixin, TemplateView): template_name = "utilities/worker_status.html"
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-rh67-4c8j-hjjhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49143ghsaADVISORY
- github.com/nautobot/nautobot/commit/9c892dc300429948a4714f743c9c2879d8987340ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/commit/d99a53b065129cff3a0fa9abe7355a9ef1ad4c95ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/6672ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/pull/6703ghsax_refsource_MISCWEB
- github.com/nautobot/nautobot/security/advisories/GHSA-rh67-4c8j-hjjhghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.