VYPR
Moderate severityNVD Advisory· Published Jun 10, 2025· Updated Jun 10, 2025

Nautobot may allows uploaded media files to be accessible without authentication

CVE-2025-49143

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.

PackageAffected versionsPatched versions
nautobotPyPI
< 1.6.321.6.32
nautobotPyPI
>= 2.0.0, < 2.4.102.4.10

Affected products

3

Patches

2
d99a53b06512

[LTM] require authentication for media files (#6703)

https://github.com/nautobot/nautobotGlenn MatthewsJun 4, 2025via ghsa
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
    
9c892dc30042

Requesting media files requires authentication (#6672)

https://github.com/nautobot/nautobotTimizuoJun 2, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.