{"record":{"id":"9b70ad79a4cd9292","repo":"aio-libs/aiohttp","slug":"403-forbidden","errorCode":null,"errorMessage":"403: Forbidden","messagePattern":"403: Forbidden","errorType":"http","errorClass":"HTTPForbidden","httpStatus":403,"severity":"warning","filePath":"aiohttp/web_urldispatcher.py","lineNumber":665,"sourceCode":"            else:\n                file_path = unresolved_path.resolve()\n                file_path.relative_to(self._directory)\n        except (ValueError, *CIRCULAR_SYMLINK_ERROR) as error:\n            # ValueError is raised for the relative check. Circular symlinks\n            # raise here on resolving for python < 3.13.\n            raise HTTPNotFound() from error\n\n        # if path is a directory, return the contents if permitted. Note the\n        # directory check will raise if a segment is not readable.\n        try:\n            if file_path.is_dir():\n                if self._show_index:\n                    return Response(\n                        text=self._directory_as_html(file_path),\n                        content_type=\"text/html\",\n                    )\n                else:\n                    raise HTTPForbidden()\n        except PermissionError as error:\n            raise HTTPForbidden() from error\n\n        # Return the file response, which handles all other checks.\n        return FileResponse(file_path, chunk_size=self._chunk_size)\n\n    def _directory_as_html(self, dir_path: Path) -> str:\n        \"\"\"returns directory's index as html.\"\"\"\n        assert dir_path.is_dir()\n\n        relative_path_to_dir = dir_path.relative_to(self._directory).as_posix()\n        index_of = f\"Index of /{html_escape(relative_path_to_dir)}\"\n        h1 = f\"<h1>{index_of}</h1>\"\n\n        index_list = []\n        dir_index = dir_path.iterdir()\n        for _file in sorted(dir_index):\n            # show file url as relative to static path","sourceCodeStart":647,"sourceCodeEnd":683,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/web_urldispatcher.py#L647-L683","documentation":"When the resolved path inside StaticResource is a directory and show_index is False (the default), aiohttp raises HTTPForbidden (403) rather than listing the directory contents. This prevents unintentional directory listing, which can leak sensitive filenames. The same 403 is also raised if a PermissionError occurs while checking is_dir() (e.g. the segment is not readable).","triggerScenarios":"A GET request to a static URL that maps to a directory (e.g. GET /static/subdir/ where subdir is a folder) when add_static was configured with show_index=False (default). Also when the OS denies read permission on a path segment, causing PermissionError during the is_dir() check.","commonSituations":"Default static configuration that does not enable directory listings; a request for a folder URL without a trailing index file; permission misconfiguration on the served directory; security probing of folder URLs.","solutions":["If you want directory listings, pass show_index=True to add_static.","Otherwise, this 403 is expected — ensure clients request specific files, not directories.","Fix filesystem permissions if PermissionError is the cause."],"exampleFix":"// before\napp.router.add_static('/static', '/app/static')  # show_index defaults to False\n// after (enable listings)\napp.router.add_static('/static', '/app/static', show_index=True)","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef require_file_or_listing(path: Path, show_index: bool):\n    if path.is_dir() and not show_index:\n        raise PermissionError('Directory listing disabled')\n","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Decide upfront whether directory listings are acceptable; default to show_index=False for security.","Add an index.html in directories you want to appear navigable.","Return a redirect to a default file for directory requests if listings are off."],"tags":["static-files","directory-listing","security","http-403"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}