CVE-2022-21712 (Python)

Twisted's HTTP redirect-following clients, including twisted.web.client.RedirectAgent and BrowserLikeRedirectAgent, can leak credential-bearing request headers when they automatically follow a redirect from one origin to another. An attacker who can cause a request carrying cookies or authorization credentials to receive a redirect to an attacker-controlled URL may receive headers such as Cookie, Cookie2, Authorization, or Proxy-Authorization on the redirected request. This exposes user/session credentials to a different scheme, host, or port than the one for which they were intended.

Weakness:
CWE-200: name: Exposure of Sensitive Information to an Unauthorized Actor
description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

Repository: https://github.com/twisted/twisted

Vulnerable code:
file_path: src/twisted/web/client.py
snippet:     def __init__(self, agent, redirectLimit=20):
        self._agent = agent
        self._redirectLimit = redirectLimit

file_path: src/twisted/web/client.py
snippet:     def _handleRedirect(self, response, method, uri, headers, redirectCount):
        """
        Handle a redirect response, checking the number of redirects already
        followed, and extracting the location header fields.
        """
        if redirectCount >= self._redirectLimit:
            err = error.InfiniteRedirection(
                response.code, b"Infinite redirection detected", location=uri
            )
            raise ResponseFailed([Failure(err)], response)
        locationHeaders = response.headers.getRawHeaders(b"location", [])
        if not locationHeaders:
            err = error.RedirectWithNoLocation(
                response.code, b"No location header field", uri
            )
            raise ResponseFailed([Failure(err)], response)
        location = self._resolveLocation(uri, locationHeaders[0])
        deferred = self._agent.request(method, location, headers)

Fixed code:
file_path: src/twisted/web/client.py
snippet: _canonicalHeaderName = Headers()._canonicalNameCaps
_defaultSensitiveHeaders = frozenset(
    [
        b"Authorization",
        b"Cookie",
        b"Cookie2",
        b"Proxy-Authorization",
        b"WWW-Authenticate",
    ]
)


@implementer(IAgent)
class RedirectAgent:
    """
    An L{Agent} wrapper which handles HTTP redirects.

    The implementation is rather strict: 301 and 302 behaves like 307, not
    redirecting automatically on methods different from I{GET} and I{HEAD}.

    See L{BrowserLikeRedirectAgent} for a redirecting Agent that behaves more
    like a web browser.

    @param redirectLimit: The maximum number of times the agent is allowed to
        follow redirects before failing with a L{error.InfiniteRedirection}.

    @param sensitiveHeaderNames: An iterable of C{bytes} enumerating the names
        of headers that must not be transmitted when redirecting to a different
        origins.  These will be consulted in addition to the protocol-specified
        set of headers that contain sensitive information.

    @cvar _redirectResponses: A L{list} of HTTP status codes to be redirected
        for I{GET} and I{HEAD} methods.

    @cvar _seeOtherResponses: A L{list} of HTTP status codes to be redirected
        for any method and the method altered to I{GET}.

    @since: 11.1
    """

    _redirectResponses = [
        http.MOVED_PERMANENTLY,
        http.FOUND,
        http.TEMPORARY_REDIRECT,
        http.PERMANENT_REDIRECT,
    ]
    _seeOtherResponses = [http.SEE_OTHER]

    def __init__(
        self,
        agent: IAgent,
        redirectLimit: int = 20,
        sensitiveHeaderNames: Iterable[bytes] = (),
    ):
        self._agent = agent
        self._redirectLimit = redirectLimit
        sensitive = {_canonicalHeaderName(each) for each in sensitiveHeaderNames}
        sensitive.update(_defaultSensitiveHeaders)
        self._sensitiveHeaderNames = sensitive

file_path: src/twisted/web/client.py
snippet:     def _handleRedirect(self, response, method, uri, headers, redirectCount):
        """
        Handle a redirect response, checking the number of redirects already
        followed, and extracting the location header fields.
        """
        if redirectCount >= self._redirectLimit:
            err = error.InfiniteRedirection(
                response.code, b"Infinite redirection detected", location=uri
            )
            raise ResponseFailed([Failure(err)], response)
        locationHeaders = response.headers.getRawHeaders(b"location", [])
        if not locationHeaders:
            err = error.RedirectWithNoLocation(
                response.code, b"No location header field", uri
            )
            raise ResponseFailed([Failure(err)], response)
        location = self._resolveLocation(uri, locationHeaders[0])
        if headers:
            parsedURI = URI.fromBytes(uri)
            parsedLocation = URI.fromBytes(location)
            sameOrigin = (
                (parsedURI.scheme == parsedLocation.scheme)
                and (parsedURI.host == parsedLocation.host)
                and (parsedURI.port == parsedLocation.port)
            )
            if not sameOrigin:
                headers = Headers(
                    {
                        rawName: rawValue
                        for rawName, rawValue in headers.getAllRawHeaders()
                        if rawName not in self._sensitiveHeaderNames
                    }
                )
        deferred = self._agent.request(method, location, headers)

        def _chainResponse(newResponse):
            newResponse.setPreviousResponse(response)
            return newResponse

        deferred.addCallback(_chainResponse)
        return deferred.addCallback(
            self._handleResponse, method, uri, headers, redirectCount + 1
        )