aio-libs/aiohttp · warning · HTTPNotFound

404: Not Found

Error message

404: Not Found

What it means

Inside StaticResource._handle, if the matched filename is an absolute path (e.g. '//network/share' on Linux or 'D:\path' on Windows), aiohttp raises HTTPNotFound (404). Absolute filenames could escape the static root and, on Windows, reference a UNC path (//server/share) which is a known NTLM credential-theft vector. The 404 is a security guard masquerading as a normal not-found.

Source

Thrown at aiohttp/web_urldispatcher.py:630

        allowed_methods = self._allowed_methods
        if method not in allowed_methods:
            return None, allowed_methods

        match_dict = {"filename": _unquote_path_safe(path[len(self._prefix) + 1 :])}
        return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods)

    def __len__(self) -> int:
        return len(self._routes)

    def __iter__(self) -> Iterator[AbstractRoute]:
        return iter(self._routes.values())

    async def _handle(self, request: Request) -> StreamResponse:
        filename = request.match_info["filename"]
        if Path(filename).is_absolute():
            # filename is an absolute path e.g. //network/share or D:\path
            # which could be a UNC path leading to NTLM credential theft
            raise HTTPNotFound()
        unresolved_path = self._directory.joinpath(filename)
        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(
            None, self._resolve_path_to_response, unresolved_path
        )

    def _resolve_path_to_response(self, unresolved_path: Path) -> StreamResponse:
        """Take the unresolved path and query the file system to form a response."""
        # Check for access outside the root directory. When the sandbox is
        # broken, URI cannot traverse out, but symlinks can. Otherwise, no
        # access outside root is permitted.
        try:
            if self._break_symlink_sandbox:
                normalized_path = Path(os.path.normpath(unresolved_path))
                normalized_path.relative_to(self._directory)
                file_path = normalized_path.resolve()
            else:
                file_path = unresolved_path.resolve()

View on GitHub (pinned to d041d4d0fd)

Solutions

  1. This is the correct security behavior — no action needed on the server; the 404 protects you.
  2. If legitimate clients hit this, fix the client URL construction to avoid leading slashes in the filename portion.
  3. Ensure your reverse proxy strips or rejects paths that would produce absolute filenames.
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_safe_static_filename(filename: str) -> bool:
    return not Path(filename).is_absolute()

Prevention

When it happens

Trigger: A request whose resolved filename (after the static prefix is stripped) is absolute — for example GET /static//etc/passwd where the filename component becomes '/etc/passwd', or a Windows UNC-style path. Also triggered by URL-encoded separators that decode to an absolute path.

Common situations: Path-traversal attempts against a static file endpoint; misconfigured reverse proxy that forwards a leading slash; client libraries that normalize URLs in unexpected ways; security scanners probing the static route.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/d18c837e710361ca. Report an issue: GitHub.