Skip to content

Insecure algorithm for determining remote/scheme/host #2171

Closed
@vfaronov

Description

Long story short

aiohttp.web.BaseRequest has attributes scheme, host and, since 2.3, remote. Their values are determined by looking at request headers that can be manipulated by a remote user. Under most configurations, the user can set them to any desired values. This is especially bad for the remote attribute, because the user’s IP address is often relied upon for access controls.

(I’m reporting this in the open rather than privately, because remote is not yet released, while scheme and host do not seem like much of a security problem.)

Expected behaviour

This involves the headers Forwarded and X-Forwarded-For. The last element (comma-separated) in these headers can be trusted as long as a trusted proxy is configured to append it.

The same applies to the headers X-Forwarded-Host and X-Forwarded-Proto, with the exception that they are usually single values (to be replaced by the proxy), not comma-separated lists.

Actual behaviour

aiohttp takes the first element from Forwarded/X-Forwarded-For, without knowing which of these headers (if any) are controlled by a trusted proxy.

In most deployments where aiohttp sits behind nginx, the Forwarded header is not controlled by the proxy. Nobody is aware that it needs to be controlled. It is not mentioned in the example nginx configuration from aiohttp docs. In such deployments, an external user can trivially force scheme, host and remote to any desired values by sending a header like:

Forwarded: for=10.0.0.1;host=example.net;proto=https

In fact, it’s impossible to configure current versions of nginx to correctly append a Forwarded header (without writing some C or possibly Lua code).

But even if aiohttp was sitting behind a proxy that correctly controlled all of the involved headers, nothing would change, because the proxy would append a comma-separated element to the remote user’s Forwarded header, while aiohttp is looking at the first element — which is still controlled by the user.

Steps to reproduce

Run this server program:

from aiohttp import web
async def handle(request):
    info = (request.scheme, request.host, request.remote)
    return web.Response(text=repr(info))
app = web.Application()
app.router.add_get('/', handle)
web.run_app(app)

behind nginx with the following configuration (derived from the example):

daemon off;
error_log stderr;
pid /tmp/nginx1.pid;
events {
}
http {
  server {
    listen 12345;
    server_name example.com;
    access_log /tmp/nginx1.access.log;
    location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_redirect off;
      proxy_buffering off;
      proxy_pass http://localhost:8080;
    }
  }
}

and send requests to it with curl:

$ curl -s localhost:12345/ -H 'Forwarded: for=10.0.0.1;host=example.net;proto=https'
('https', 'example.net', '10.0.0.1')

Your environment

aiohttp Git master, Python 3.5, Linux

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions