VYPR
High severityNVD Advisory· Published Aug 4, 2009· Updated Apr 23, 2026

CVE-2009-2659

CVE-2009-2659

Description

The Admin media handler in core/servers/basehttp.py in Django 1.0 and 0.96 does not properly map URL requests to expected "static media files," which allows remote attackers to conduct directory traversal attacks and read arbitrary files via a crafted URL.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
DjangoPyPI
>= 0.96.0, < 0.96.40.96.4
DjangoPyPI
>= 1.0, < 1.0.31.0.3

Affected products

2
  • cpe:2.3:a:django_project:django:0.96:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:django_project:django:0.96:*:*:*:*:*:*:*
    • cpe:2.3:a:django_project:django:1.0:*:*:*:*:*:*:*

Patches

2
da85d76fd6ca

[0.96.X] SECURITY ALERT: Corrected a problem with the Admin media handler that could lead to the exposure of system files. Thanks to Gary Wilson for the patch.

https://github.com/django/djangoRussell Keith-MageeJul 29, 2009via ghsa
5 files changed · +95 8
  • django/core/management.py+1 3 modified
    @@ -1192,9 +1192,7 @@ def inner_run():
             print "Development server is running at http://%s:%s/" % (addr, port)
             print "Quit the server with %s." % quit_command
             try:
    -            import django
    -            path = admin_media_dir or django.__path__[0] + '/contrib/admin/media'
    -            handler = AdminMediaHandler(WSGIHandler(), path)
    +            handler = AdminMediaHandler(WSGIHandler(), admin_media_path)
                 run(addr, int(port), handler)
             except WSGIServerException, e:
                 # Use helpful error messages instead of ugly tracebacks.
    
  • django/core/servers/basehttp.py+27 5 modified
    @@ -11,6 +11,8 @@
     from types import ListType, StringType
     import os, re, sys, time, urllib
     
    +from django.utils._os import safe_join
    +
     __version__ = "0.1"
     __all__ = ['WSGIServer','WSGIRequestHandler','demo_app']
     
    @@ -599,11 +601,25 @@ def __init__(self, application, media_dir=None):
             self.application = application
             if not media_dir:
                 import django
    -            self.media_dir = django.__path__[0] + '/contrib/admin/media'
    +            self.media_dir = \
    +                os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
             else:
                 self.media_dir = media_dir
             self.media_url = settings.ADMIN_MEDIA_PREFIX
     
    +    def file_path(self, url):
    +        """
    +        Returns the path to the media file on disk for the given URL.
    +
    +        The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX.  If the
    +        resultant file path is outside the media directory, then a ValueError
    +        is raised.
    +        """
    +        # Remove ADMIN_MEDIA_PREFIX.
    +        relative_url = url[len(self.media_url):]
    +        relative_path = urllib.url2pathname(relative_url)
    +        return safe_join(self.media_dir, relative_path)
    +
         def __call__(self, environ, start_response):
             import os.path
     
    @@ -614,19 +630,25 @@ def __call__(self, environ, start_response):
                 return self.application(environ, start_response)
     
             # Find the admin file and serve it up, if it exists and is readable.
    -        relative_url = environ['PATH_INFO'][len(self.media_url):]
    -        file_path = os.path.join(self.media_dir, relative_url)
    +        try:
    +            file_path = self.file_path(environ['PATH_INFO'])
    +        except ValueError: # Resulting file path was not valid.
    +            status = '404 NOT FOUND'
    +            headers = {'Content-type': 'text/plain'}
    +            output = ['Page not found: %s' % environ['PATH_INFO']]
    +            start_response(status, headers.items())
    +            return output
             if not os.path.exists(file_path):
                 status = '404 NOT FOUND'
                 headers = {'Content-type': 'text/plain'}
    -            output = ['Page not found: %s' % file_path]
    +            output = ['Page not found: %s' % environ['PATH_INFO']]
             else:
                 try:
                     fp = open(file_path, 'rb')
                 except IOError:
                     status = '401 UNAUTHORIZED'
                     headers = {'Content-type': 'text/plain'}
    -                output = ['Permission denied: %s' % file_path]
    +                output = ['Permission denied: %s' % environ['PATH_INFO']]
                 else:
                     status = '200 OK'
                     headers = {}
    
  • tests/regressiontests/servers/__init__.py+0 0 added
  • tests/regressiontests/servers/models.py+0 0 added
  • tests/regressiontests/servers/tests.py+67 0 added
    @@ -0,0 +1,67 @@
    +"""
    +Tests for django.core.servers.
    +"""
    +
    +import os
    +
    +import django
    +from django.test import TestCase
    +from django.core.handlers.wsgi import WSGIHandler
    +from django.core.servers.basehttp import AdminMediaHandler
    +
    +
    +class AdminMediaHandlerTests(TestCase):
    +
    +    def setUp(self):
    +        self.admin_media_file_path = \
    +            os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
    +        self.handler = AdminMediaHandler(WSGIHandler())
    +
    +    def test_media_urls(self):
    +        """
    +        Tests that URLs that look like absolute file paths after the
    +        settings.ADMIN_MEDIA_PREFIX don't turn into absolute file paths.
    +        """
    +        # Cases that should work on all platforms.
    +        data = (
    +            ('/media/css/base.css', ('css', 'base.css')),
    +        )
    +        # Cases that should raise an exception.
    +        bad_data = ()
    +
    +        # Add platform-specific cases.
    +        if os.sep == '/':
    +            data += (
    +                # URL, tuple of relative path parts.
    +                ('/media/\\css/base.css', ('\\css', 'base.css')),
    +            )
    +            bad_data += (
    +                '/media//css/base.css',
    +                '/media////css/base.css',
    +                '/media/../css/base.css',
    +            )
    +        elif os.sep == '\\':
    +            bad_data += (
    +                '/media/C:\css/base.css',
    +                '/media//\\css/base.css',
    +                '/media/\\css/base.css',
    +                '/media/\\\\css/base.css'
    +            )
    +        for url, path_tuple in data:
    +            try:
    +                output = self.handler.file_path(url)
    +            except ValueError:
    +                self.fail("Got a ValueError exception, but wasn't expecting"
    +                          " one. URL was: %s" % url)
    +            rel_path = os.path.join(*path_tuple)
    +            desired = os.path.normcase(
    +                os.path.join(self.admin_media_file_path, rel_path))
    +            self.assertEqual(output, desired,
    +                "Got: %s, Expected: %s, URL was: %s" % (output, desired, url))
    +        for url in bad_data:
    +            try:
    +                output = self.handler.file_path(url)
    +            except ValueError:
    +                continue
    +            self.fail('URL: %s should have caused a ValueError exception.'
    +                      % url)
    
df7f917b7f51

[1.0.X] SECURITY ALERT: Corrected a problem with the Admin media handler that could lead to the exposure of system files. Thanks to Gary Wilson for the patch.

https://github.com/django/djangoRussell Keith-MageeJul 29, 2009via ghsa
5 files changed · +94 7
  • django/core/management/commands/runserver.py+1 2 modified
    @@ -56,8 +56,7 @@ def inner_run():
                 translation.activate(settings.LANGUAGE_CODE)
     
                 try:
    -                path = admin_media_path or django.__path__[0] + '/contrib/admin/media'
    -                handler = AdminMediaHandler(WSGIHandler(), path)
    +                handler = AdminMediaHandler(WSGIHandler(), admin_media_path)
                     run(addr, int(port), handler)
                 except WSGIServerException, e:
                     # Use helpful error messages instead of ugly tracebacks.
    
  • django/core/servers/basehttp.py+26 5 modified
    @@ -16,6 +16,7 @@
     import urllib
     
     from django.utils.http import http_date
    +from django.utils._os import safe_join
     
     __version__ = "0.1"
     __all__ = ['WSGIServer','WSGIRequestHandler']
    @@ -621,11 +622,25 @@ def __init__(self, application, media_dir=None):
             self.application = application
             if not media_dir:
                 import django
    -            self.media_dir = django.__path__[0] + '/contrib/admin/media'
    +            self.media_dir = \
    +                os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
             else:
                 self.media_dir = media_dir
             self.media_url = settings.ADMIN_MEDIA_PREFIX
     
    +    def file_path(self, url):
    +        """
    +        Returns the path to the media file on disk for the given URL.
    +
    +        The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX.  If the
    +        resultant file path is outside the media directory, then a ValueError
    +        is raised.
    +        """
    +        # Remove ADMIN_MEDIA_PREFIX.
    +        relative_url = url[len(self.media_url):]
    +        relative_path = urllib.url2pathname(relative_url)
    +        return safe_join(self.media_dir, relative_path)
    +
         def __call__(self, environ, start_response):
             import os.path
     
    @@ -636,19 +651,25 @@ def __call__(self, environ, start_response):
                 return self.application(environ, start_response)
     
             # Find the admin file and serve it up, if it exists and is readable.
    -        relative_url = environ['PATH_INFO'][len(self.media_url):]
    -        file_path = os.path.join(self.media_dir, relative_url)
    +        try:
    +            file_path = self.file_path(environ['PATH_INFO'])
    +        except ValueError: # Resulting file path was not valid.
    +            status = '404 NOT FOUND'
    +            headers = {'Content-type': 'text/plain'}
    +            output = ['Page not found: %s' % environ['PATH_INFO']]
    +            start_response(status, headers.items())
    +            return output
             if not os.path.exists(file_path):
                 status = '404 NOT FOUND'
                 headers = {'Content-type': 'text/plain'}
    -            output = ['Page not found: %s' % file_path]
    +            output = ['Page not found: %s' % environ['PATH_INFO']]
             else:
                 try:
                     fp = open(file_path, 'rb')
                 except IOError:
                     status = '401 UNAUTHORIZED'
                     headers = {'Content-type': 'text/plain'}
    -                output = ['Permission denied: %s' % file_path]
    +                output = ['Permission denied: %s' % environ['PATH_INFO']]
                 else:
                     # This is a very simple implementation of conditional GET with
                     # the Last-Modified header. It makes media files a bit speedier
    
  • tests/regressiontests/servers/__init__.py+0 0 added
  • tests/regressiontests/servers/models.py+0 0 added
  • tests/regressiontests/servers/tests.py+67 0 added
    @@ -0,0 +1,67 @@
    +"""
    +Tests for django.core.servers.
    +"""
    +
    +import os
    +
    +import django
    +from django.test import TestCase
    +from django.core.handlers.wsgi import WSGIHandler
    +from django.core.servers.basehttp import AdminMediaHandler
    +
    +
    +class AdminMediaHandlerTests(TestCase):
    +
    +    def setUp(self):
    +        self.admin_media_file_path = \
    +            os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
    +        self.handler = AdminMediaHandler(WSGIHandler())
    +
    +    def test_media_urls(self):
    +        """
    +        Tests that URLs that look like absolute file paths after the
    +        settings.ADMIN_MEDIA_PREFIX don't turn into absolute file paths.
    +        """
    +        # Cases that should work on all platforms.
    +        data = (
    +            ('/media/css/base.css', ('css', 'base.css')),
    +        )
    +        # Cases that should raise an exception.
    +        bad_data = ()
    +
    +        # Add platform-specific cases.
    +        if os.sep == '/':
    +            data += (
    +                # URL, tuple of relative path parts.
    +                ('/media/\\css/base.css', ('\\css', 'base.css')),
    +            )
    +            bad_data += (
    +                '/media//css/base.css',
    +                '/media////css/base.css',
    +                '/media/../css/base.css',
    +            )
    +        elif os.sep == '\\':
    +            bad_data += (
    +                '/media/C:\css/base.css',
    +                '/media//\\css/base.css',
    +                '/media/\\css/base.css',
    +                '/media/\\\\css/base.css'
    +            )
    +        for url, path_tuple in data:
    +            try:
    +                output = self.handler.file_path(url)
    +            except ValueError:
    +                self.fail("Got a ValueError exception, but wasn't expecting"
    +                          " one. URL was: %s" % url)
    +            rel_path = os.path.join(*path_tuple)
    +            desired = os.path.normcase(
    +                os.path.join(self.admin_media_file_path, rel_path))
    +            self.assertEqual(output, desired,
    +                "Got: %s, Expected: %s, URL was: %s" % (output, desired, url))
    +        for url in bad_data:
    +            try:
    +                output = self.handler.file_path(url)
    +            except ValueError:
    +                continue
    +            self.fail('URL: %s should have caused a ValueError exception.'
    +                      % url)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

16

News mentions

0

No linked articles in our index yet.