Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gunicorn's handling of PATH_INFO and SCRIPT_NAME can lead to security issues when placed behind a proxy #2650

Closed
itszn opened this issue Sep 12, 2021 · 10 comments

Comments

@itszn
Copy link

itszn commented Sep 12, 2021

This might not be something you can fix, but I wanted to report it since people might have vulnerable configs.

Gunicorn reads some WSGI variables from request headers. This can be an issue when a proxy allows and passes along these special header values.

For example SCRIPT_NAME is used to transform PATH_INFO. If a proxy previously parsed the request path to determine if access is allowed, and attacker could now use a SCRIPT_NAME header to bypass the proxy.

Here is an example nginx config:

server {
    listen 80;
    underscores_in_headers on;
    location / {
        include proxy_params;
        proxy_pass http://unix:/tmp/gunicorn.sock;
        proxy_pass_request_headers on;
        location ^~ /admin/ {
            deny all; # disallow anyone from accessing any routes starting with /admin/
        }
}

This request will call the /admin/something/bad route in the WSGI app despite nginx's deny all.

requests.get(URL+'/REMOVED/admin/something/bad', headers={'script_name':'REMOVED/'})
@benoitc
Copy link
Owner

benoitc commented Sep 13, 2021

Fair enough. This is a part of the spec. I think most of the time it is harmless. But what about adding a ˋ—standalone ˋ option? that would make gunicorn safer

@apollo13
Copy link
Contributor

apollo13 commented Dec 7, 2021

@benoitc Just stumbled upon this. Which spec would mandate reading script_name from the request headers?

@javabrett
Copy link
Collaborator

@itszn thanks for documenting this.

Noting that underscores_in_headers default is off, and the potential exploit requires it to be on - slight mitigation but can be enabled without understanding the impact.

Would it be possible to document adding:

proxy_set_header SCRIPT_NAME "";

It's possible that Gunicorn isn't strictly required to read theSCRIPT_NAME header, but I expect a lot of reverse-proxy setups are going to be relying on that now by-default.

@ivanshatsky
Copy link

It seems to me that using this is the only (and a convenient) way to serve a WSGI app under a custom URI prefix without touching the app code or environment, and indeed a lot of reverse-proxy setups (including my own) are using this, so please if you are going to remove this feature at least leave an option to make this approach workable.

@douardda
Copy link

it looks like this has (more or less) been addressed by 72b8970 in v22 (which incidentally breaks such a usage of the SCRIPT_NAME set by a reverse proxy; also note that FAQ WSGI Bits/How do I set SCRIPT_NAME might need an update.

@cpburnz
Copy link

cpburnz commented Apr 23, 2024

What is the recommended alternative to using the SCRIPT_NAME header? I host multiple Gunicorn/Pyramid web applications on a single host under different paths using Nginx. Nginx sets the SCRIPT_NAME header using the proxy_set_header directive and everything has worked great for years. Upgrading from 21.2.0 to 22.0.0 breaks SCRIPT_NAME.

@douardda
Copy link

for now I believe the only solution is to declare the SCRIPT_NAME as an actual environment variable for the gunicorn process (so you cannot have the same gunicorn app served under different paths AFAIK) rather than passing from the the reverse proxy as a header.

@pajod
Copy link
Contributor

pajod commented Apr 24, 2024

What still works is transferring secure metadata out of band, unaffected by my restriction on HTTP headers - e.g. if you are launching gunicorn via systemd, tell it to put your settings into the process environment:

# gunicorn.service
[Unit]
After=nss-lookup.target
[Service]
Environment=NUM_WORKERS=8 SCRIPT_NAME=/subfolder
ExecStart=/usr/bin/python3 -m gunicorn --workers=%{NUM_WORKERS} --opt2 ...
SyslogLevelPrefix=false
...

Gunicorn also provides an option to insert arbitrary environment from its configuration or command line:

gunicorn --env SCRIPT_NAME=/subfolder --workers=2 mywsgi:application

The 21.2.0 behaviour is still available via launching gunicorn with the option --header-map dangerous. Resort to this only after carefully investigating what your application or its framework does in terms of blindly trusting other such headers. Do note that this is just one example of such underscore headers, you can play similar games with others, depending on how the application uses them. For Nginx users this investigation is straightforward - you would have to apply explicit configuration for your application to receive such header at all. For other proxies it can be much less obviously dangerous.

I suspect amending the --forwarded-allow-ips feature to selectively enable processing of headers not usually intended permissible for outsiders would serve a wider range of previous use cases of the "permit for everyone" behaviour.

@cpburnz
Copy link

cpburnz commented Apr 24, 2024

for now I believe the only solution is to declare the SCRIPT_NAME as an actual environment variable for the gunicorn process (so you cannot have the same gunicorn app served under different paths AFAIK) rather than passing from the the reverse proxy as a header.

@douardda Thanks, that works great. I was never quite sure how or why the SCRIPT_NAME header worked. It was only mentioned in one spot in the FAQ, and the WSGI spec just says it comes from the web server (reverse proxy) as a CGI variable.

For anyone wanting to set SCRIPT_PATH in the gunicorn.conf.py there's two ways:

# Directly set the env variable.
raw_env = ['SCRIPT_NAME=/my-app-path']

# Or, set a default that can be overridden by a pre-exiting env variable.
import os
os.environ.setdefault('SCRIPT_NAME', '/my-app-path')

@burnettk
Copy link

here's a minimal repro of the difference between gunicorn 21.2.0 and 22.0.0 with respect to a SCRIPT_NAME header, in case it's useful to anyone: #3200

@benoitc benoitc closed this as completed Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants