CVE-2021-33203 (Python)

Django before 2.2.24, 3.x before 3.1.12, and 3.2.x before 3.2.4 has a potential directory traversal via django.contrib.admindocs. Staff members could use the TemplateDetailView view to check the existence of arbitrary files. Additionally, if (and only if) the default admindocs templates have been customized by application developers to also show file contents, then not only the existence but also the file contents would have been exposed. In other words, there is directory traversal outside of the template root directories.

Weakness:
CWE-73: name: External Control of File Name or Path
description: The product allows user input to control or influence paths or file names that are used in filesystem operations.
CWE-22: name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Repository: https://github.com/django/django

Vulnerable code:
file_path: django/contrib/admindocs/views.py
snippet:     def get_context_data(self, **kwargs):
        template = self.kwargs['template']
        templates = []
        try:
            default_engine = Engine.get_default()
        except ImproperlyConfigured:
            # Non-trivial TEMPLATES settings aren't supported (#24125).
            pass
        else:
            # This doesn't account for template loaders (#24128).
            for index, directory in enumerate(default_engine.dirs):
                template_file = Path(directory) / template
                if template_file.exists():
                    template_contents = template_file.read_text()
                else:
                    template_contents = ''
                templates.append({
                    'file': template_file,
                    'exists': template_file.exists(),
                    'contents': template_contents,
                    'order': index,
                })
        return super().get_context_data(**{
            **kwargs,
            'name': template,
            'templates': templates,
        })

Fixed code:
file_path: django/contrib/admindocs/views.py
snippet:     def get_context_data(self, **kwargs):
        template = self.kwargs['template']
        templates = []
        try:
            default_engine = Engine.get_default()
        except ImproperlyConfigured:
            # Non-trivial TEMPLATES settings aren't supported (#24125).
            pass
        else:
            # This doesn't account for template loaders (#24128).
            for index, directory in enumerate(default_engine.dirs):
                template_file = Path(safe_join(directory, template))
                if template_file.exists():
                    template_contents = template_file.read_text()
                else:
                    template_contents = ''
                templates.append({
                    'file': template_file,
                    'exists': template_file.exists(),
                    'contents': template_contents,
                    'order': index,
                })
        return super().get_context_data(**{
            **kwargs,
            'name': template,
            'templates': templates,
        })