From 01bcdb1d126ee2f4f3f54c3144a6144d4dd56e3e Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Wed, 24 Apr 2024 08:48:27 +0200 Subject: [PATCH 1/7] Exempt SCRIPT_NAME from newly introduced --header-map treatment --- docs/source/settings.rst | 12 ++++++++---- gunicorn/config.py | 12 ++++++++---- gunicorn/http/message.py | 7 ++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/source/settings.rst b/docs/source/settings.rst index c20af3da7..1bb90d551 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -1232,17 +1232,18 @@ the headers defined here can not be passed directly from the client. **Command line:** ``--forwarded-allow-ips STRING`` -**Default:** ``'127.0.0.1'`` +**Default:** ``'127.0.0.1,::1'`` Front-end's IPs from which allowed to handle set secure headers. (comma separate). -Set to ``*`` to disable checking of Front-end IPs (useful for setups +Set to ``*`` to disable checking of Front-end IPs. This is useful for setups where you don't know in advance the IP address of Front-end, but -you still trust the environment). +instead have ensured via other means that none other than your +authorized Front-ends can access gunicorn. By default, the value of the ``FORWARDED_ALLOW_IPS`` environment -variable. If it is not defined, the default is ``"127.0.0.1"``. +variable. If it is not defined, the default is ``"127.0.0.1,::1"``. .. note:: @@ -1498,6 +1499,9 @@ The value ``refuse`` will return an error if a request contains *any* such heade The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different header field names into the same environ name. +The (at this time, not configurable) header `SCRIPT_NAME` is permitted + without consulting this setting, if it is received from an allowed forwarder. + Use with care and only if necessary and after considering if your problem could instead be solved by specifically renaming or rewriting only the intended headers on a proxy in front of Gunicorn. diff --git a/gunicorn/config.py b/gunicorn/config.py index a0366264f..209d93d85 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -1263,17 +1263,18 @@ class ForwardedAllowIPS(Setting): cli = ["--forwarded-allow-ips"] meta = "STRING" validator = validate_string_to_list - default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1") + default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1") desc = """\ Front-end's IPs from which allowed to handle set secure headers. (comma separate). - Set to ``*`` to disable checking of Front-end IPs (useful for setups + Set to ``*`` to disable checking of Front-end IPs. This is useful for setups where you don't know in advance the IP address of Front-end, but - you still trust the environment). + instead have ensured via other means that none other than your + authorized Front-ends can access gunicorn. By default, the value of the ``FORWARDED_ALLOW_IPS`` environment - variable. If it is not defined, the default is ``"127.0.0.1"``. + variable. If it is not defined, the default is ``"127.0.0.1,::1"``. .. note:: @@ -2365,6 +2366,9 @@ class HeaderMap(Setting): The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different header field names into the same environ name. + The (at this time, not configurable) header `SCRIPT_NAME` is permitted + without consulting this setting, if it is received from an allowed forwarder. + Use with care and only if necessary and after considering if your problem could instead be solved by specifically renaming or rewriting only the intended headers on a proxy in front of Gunicorn. diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 1cb48ef30..d71ba0ab8 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -78,6 +78,7 @@ def parse_headers(self, data, from_trailer=False): # handle scheme headers scheme_header = False secure_scheme_headers = {} + allowed_forwarder_headers = [] if from_trailer: # nonsense. either a request is https from the beginning # .. or we are just behind a proxy who does not remove conflicting trailers @@ -86,6 +87,7 @@ def parse_headers(self, data, from_trailer=False): not isinstance(self.peer_addr, tuple) or self.peer_addr[0] in cfg.forwarded_allow_ips): secure_scheme_headers = cfg.secure_scheme_headers + allowed_forwarder_headers = ["SCRIPT_NAME"] # Parse headers into key/value pairs paying attention # to continuation lines. @@ -144,7 +146,10 @@ def parse_headers(self, data, from_trailer=False): # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1 # Only modify after fixing *ALL* header transformations; network to wsgi env if "_" in name: - if self.cfg.header_map == "dangerous": + if name in allowed_forwarder_headers: + # This forwarder may override our environment + pass + elif self.cfg.header_map == "dangerous": # as if we did not know we cannot safely map this pass elif self.cfg.header_map == "drop": From 3e042e826935aa464a906a0ee8b4630191c8337c Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 25 Apr 2024 11:54:55 +0200 Subject: [PATCH 2/7] Configurable list of forwarder headers --- docs/source/deploy.rst | 19 ++++++++++++------- docs/source/faq.rst | 4 +++- docs/source/news.rst | 31 ++++++++++++++++++------------ docs/source/settings.rst | 26 ++++++++++++++++++++++--- gunicorn/config.py | 41 ++++++++++++++++++++++++++++++++++++---- gunicorn/http/message.py | 6 +++--- tests/test_config.py | 24 +++++++++++++++++++---- 7 files changed, 117 insertions(+), 34 deletions(-) diff --git a/docs/source/deploy.rst b/docs/source/deploy.rst index 6871421ab..c6bdbe114 100644 --- a/docs/source/deploy.rst +++ b/docs/source/deploy.rst @@ -246,20 +246,24 @@ to the newly created unix socket: After=network.target [Service] + # gunicorn can let systemd know when it is ready Type=notify + NotifyAccess=main # the specific user that our service will run as User=someuser Group=someuser - # another option for an even more restricted service is - # DynamicUser=yes - # see http://0pointer.net/blog/dynamic-users-with-systemd.html + # this user can be transiently created by systemd + # DynamicUser=true RuntimeDirectory=gunicorn + WorkingDirectory=~ WorkingDirectory=/home/someuser/applicationroot ExecStart=/usr/bin/gunicorn applicationname.wsgi ExecReload=/bin/kill -s HUP $MAINPID KillMode=mixed TimeoutStopSec=5 PrivateTmp=true + # if your app does not need administrative capabilities, let systemd know + # ProtectSystem=strict [Install] WantedBy=multi-user.target @@ -272,11 +276,12 @@ to the newly created unix socket: [Socket] ListenStream=/run/gunicorn.sock # Our service won't need permissions for the socket, since it - # inherits the file descriptor by socket activation - # only the nginx daemon will need access to the socket + # inherits the file descriptor by socket activation. + # Only the nginx daemon will need access to the socket: SocketUser=www-data - # Optionally restrict the socket permissions even more. - # SocketMode=600 + SocketGroup=www-data + # Once the user/group is correct, restrict the permissions: + SocketMode=0660 [Install] WantedBy=sockets.target diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 64b44c905..10572c1d4 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -11,7 +11,9 @@ How do I set SCRIPT_NAME? ------------------------- By default ``SCRIPT_NAME`` is an empty string. The value could be set by -setting ``SCRIPT_NAME`` in the environment or as an HTTP header. +setting ``SCRIPT_NAME`` in the environment or as an HTTP header. Note that +this headers contains and underscore, so it is only accepted from trusted +forwarders listed in the ``forwarded-allow-ips`` setting. Server Stuff diff --git a/docs/source/news.rst b/docs/source/news.rst index 431a01b2c..c28f6d9c6 100644 --- a/docs/source/news.rst +++ b/docs/source/news.rst @@ -5,20 +5,27 @@ Changelog 23.0.0 - unreleased =================== -* minor docs fixes (:pr:`3217`, :pr:`3089`, :pr:`3167`) -* worker_class parameter accepts a class (:pr:`3079`) -* fix deadlock if request terminated during chunked parsing (:pr:`2688`) -* permit receiving Transfer-Encodings: compress, deflate, gzip (:pr:`3261`) -* permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still (:pr:`3261`) -* sdist generation now explicitly excludes sphinx build folder (:pr:`3257`) -* decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` (:pr:`2336`) -* raise correct Exception when encounting invalid chunked requests (:pr:`3258`) +- minor docs fixes (:pr:`3217`, :pr:`3089`, :pr:`3167`) +- worker_class parameter accepts a class (:pr:`3079`) +- fix deadlock if request terminated during chunked parsing (:pr:`2688`) +- permit receiving Transfer-Encodings: compress, deflate, gzip (:pr:`3261`) +- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still (:pr:`3261`) +- sdist generation now explicitly excludes sphinx build folder (:pr:`3257`) +- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` (:pr:`2336`) +- raise correct Exception when encounting invalid chunked requests (:pr:`3258`) +- the SCRIPT_NAME header when received from allowed forwarders is no longer restricted for containing an underscore (:pr:`3192`) + +** NOTE ** + +- The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release +- Review your ``forwarded-allow-ips`` setting if you are still not seeing the SCRIPT_NAME transmitted ** Breaking changes ** -* refuse requests where the uri field is empty (:pr:`3255`) -* refuse requests with invalid CR/LR/NUL in heade field values (:pr:`3253`) -* remove temporary `--tolerate-dangerous-framing` switch from 22.0 (:pr:`3260`) -* If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies. + +- refuse requests where the uri field is empty (:pr:`3255`) +- refuse requests with invalid CR/LR/NUL in heade field values (:pr:`3253`) +- remove temporary `--tolerate-dangerous-framing` switch from 22.0 (:pr:`3260`) +- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies. 22.0.0 - 2024-04-17 =================== diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 1bb90d551..67a473c54 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -1479,6 +1479,25 @@ Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 .. versionadded:: 22.0.0 +.. _forwarder-headers: + +``forwarder_headers`` +~~~~~~~~~~~~~~~~~~~~~ + +**Command line:** ``--forwarder-headers`` + +**Default:** ``'SCRIPT_NAME'`` + +A list containing upper-case header field names that the front-end proxy +sets, to be used in WSGI environment. + +If headers named in this list are not present in the request, they will be ignored. + +This option can be used to transfer SCRIPT_NAME and REMOTE_USER. + +It is important that your front-end proxy configuration ensures that +the headers defined here can not be passed directly from the client. + .. _header-map: ``header_map`` @@ -1496,11 +1515,12 @@ the same environment variable will dangerously confuse applications as to which The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. The value ``refuse`` will return an error if a request contains *any* such header. -The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different +The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different header field names into the same environ name. -The (at this time, not configurable) header `SCRIPT_NAME` is permitted - without consulting this setting, if it is received from an allowed forwarder. +If the source IP is permitted by ``forwarded-allow-ips``, *and* the header name is +present in ``forwarder-headers``, the header is mapped into environment regardless of +the state of this setting. Use with care and only if necessary and after considering if your problem could instead be solved by specifically renaming or rewriting only the intended headers diff --git a/gunicorn/config.py b/gunicorn/config.py index 209d93d85..f047074b8 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -9,6 +9,7 @@ import copy import grp import inspect +import ipaddress import os import pwd import re @@ -402,6 +403,17 @@ def validate_list_of_existing_files(val): return [validate_file_exists(v) for v in validate_list_string(val)] +def validate_string_to_addr_list(val): + val = validate_string_to_list(val) + + for addr in val: + if addr == "*": + continue + _vaid_ip = ipaddress.ip_address(addr) + + return val + + def validate_string_to_list(val): val = validate_string(val) @@ -1262,7 +1274,7 @@ class ForwardedAllowIPS(Setting): section = "Server Mechanics" cli = ["--forwarded-allow-ips"] meta = "STRING" - validator = validate_string_to_list + validator = validate_string_to_addr_list default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1") desc = """\ Front-end's IPs from which allowed to handle set secure headers. @@ -2348,6 +2360,26 @@ def validate_header_map_behaviour(val): raise ValueError("Invalid header map behaviour: %s" % val) +class ForwarderHeaders(Setting): + name = "forwarder_headers" + section = "Server Mechanics" + cli = ["--forwarder-headers"] + validator = validate_string_to_list + default = "SCRIPT_NAME" + desc = """\ + + A list containing upper-case header field names that the front-end proxy + sets, to be used in WSGI environment. + + If headers named in this list are not present in the request, they will be ignored. + + This option can be used to transfer SCRIPT_NAME and REMOTE_USER. + + It is important that your front-end proxy configuration ensures that + the headers defined here can not be passed directly from the client. + """ + + class HeaderMap(Setting): name = "header_map" section = "Server Mechanics" @@ -2363,11 +2395,12 @@ class HeaderMap(Setting): The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. The value ``refuse`` will return an error if a request contains *any* such header. - The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different + The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different header field names into the same environ name. - The (at this time, not configurable) header `SCRIPT_NAME` is permitted - without consulting this setting, if it is received from an allowed forwarder. + If the source IP is permitted by ``forwarded-allow-ips``, *and* the header name is + present in ``forwarder-headers``, the header is mapped into environment regardless of + the state of this setting. Use with care and only if necessary and after considering if your problem could instead be solved by specifically renaming or rewriting only the intended headers diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index d71ba0ab8..d65aebe5c 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -78,7 +78,7 @@ def parse_headers(self, data, from_trailer=False): # handle scheme headers scheme_header = False secure_scheme_headers = {} - allowed_forwarder_headers = [] + forwarder_headers = [] if from_trailer: # nonsense. either a request is https from the beginning # .. or we are just behind a proxy who does not remove conflicting trailers @@ -87,7 +87,7 @@ def parse_headers(self, data, from_trailer=False): not isinstance(self.peer_addr, tuple) or self.peer_addr[0] in cfg.forwarded_allow_ips): secure_scheme_headers = cfg.secure_scheme_headers - allowed_forwarder_headers = ["SCRIPT_NAME"] + forwarder_headers = cfg.forwarder_headers # Parse headers into key/value pairs paying attention # to continuation lines. @@ -146,7 +146,7 @@ def parse_headers(self, data, from_trailer=False): # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1 # Only modify after fixing *ALL* header transformations; network to wsgi env if "_" in name: - if name in allowed_forwarder_headers: + if name in forwarder_headers: # This forwarder may override our environment pass elif self.cfg.header_map == "dangerous": diff --git a/tests/test_config.py b/tests/test_config.py index f0ab392cd..9afb4e49c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -164,16 +164,32 @@ def test_str_validation(): pytest.raises(TypeError, c.set, "proc_name", 2) -def test_str_to_list_validation(): +def test_str_to_addr_list_validation(): c = config.Config() - assert c.forwarded_allow_ips == ["127.0.0.1"] - c.set("forwarded_allow_ips", "127.0.0.1,192.168.0.1") - assert c.forwarded_allow_ips == ["127.0.0.1", "192.168.0.1"] + assert c.forwarded_allow_ips == ["127.0.0.1", "::1"] + c.set("forwarded_allow_ips", "127.0.0.1,192.0.2.1") + assert c.forwarded_allow_ips == ["127.0.0.1", "192.0.2.1"] c.set("forwarded_allow_ips", "") assert c.forwarded_allow_ips == [] c.set("forwarded_allow_ips", None) assert c.forwarded_allow_ips == [] + # demand addresses are specified unambiguously pytest.raises(TypeError, c.set, "forwarded_allow_ips", 1) + # demand networks are specified unambiguously + pytest.raises(ValueError, c.set, "forwarded_allow_ips", "127.0.0") + # detect typos + pytest.raises(ValueError, c.set, "forwarded_allow_ips", "::f:") + + +def test_str_to_list(): + c = config.Config() + assert c.forwarder_headers == ["SCRIPT_NAME"] + c.set("forwarder_headers", "SCRIPT_NAME,REMOTE_USER") + assert c.forwarder_headers == ["SCRIPT_NAME", "REMOTE_USER"] + c.set("forwarder_headers", "") + assert c.forwarder_headers == [] + c.set("forwarder_headers", None) + assert c.forwarder_headers == [] def test_callable_validation(): From 5bbf373e181e11270e09db6987980c02931b7ae5 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 25 Apr 2024 12:23:34 +0200 Subject: [PATCH 3/7] undocumented allow-all for forwarder-headers --- gunicorn/http/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index d65aebe5c..1f2875abe 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -146,7 +146,7 @@ def parse_headers(self, data, from_trailer=False): # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1 # Only modify after fixing *ALL* header transformations; network to wsgi env if "_" in name: - if name in forwarder_headers: + if name in forwarder_headers or "*" in forwarder_headers: # This forwarder may override our environment pass elif self.cfg.header_map == "dangerous": From 687b78d20c1270e2789401dce1d5111d8c10b6b5 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 8 Aug 2024 18:15:34 +0200 Subject: [PATCH 4/7] config defaults: PATH_NAME and ::1 for proxy * PATH_NAME is used like SCRIPT_NAME: include both * replicate changed forwarded-allow-ips default to proxy_allow_ips --- docs/source/news.rst | 8 ++++--- docs/source/settings.rst | 46 ++++++++++++++++++++++++-------------- gunicorn/config.py | 48 +++++++++++++++++++++++++--------------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/docs/source/news.rst b/docs/source/news.rst index c28f6d9c6..28fecabb7 100644 --- a/docs/source/news.rst +++ b/docs/source/news.rst @@ -13,18 +13,20 @@ Changelog - sdist generation now explicitly excludes sphinx build folder (:pr:`3257`) - decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` (:pr:`2336`) - raise correct Exception when encounting invalid chunked requests (:pr:`3258`) -- the SCRIPT_NAME header when received from allowed forwarders is no longer restricted for containing an underscore (:pr:`3192`) +- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore (:pr:`3192`) +- include IPv6 loopback address ``[::1]`` in default for :ref:`forwarded-allow-ips` and :ref:`proxy-allow-ips` (:pr:`3192`) ** NOTE ** - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release -- Review your ``forwarded-allow-ips`` setting if you are still not seeing the SCRIPT_NAME transmitted +- Review your :ref:`forwarded-allow-ips` setting if you are still not seeing the SCRIPT_NAME transmitted +- Review your :ref:`forwarder-headers` setting if you are missing headers after upgrading from a version prior to 22.0.0 ** Breaking changes ** - refuse requests where the uri field is empty (:pr:`3255`) - refuse requests with invalid CR/LR/NUL in heade field values (:pr:`3253`) -- remove temporary `--tolerate-dangerous-framing` switch from 22.0 (:pr:`3260`) +- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 (:pr:`3260`) - If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies. 22.0.0 - 2024-04-17 diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 67a473c54..fe2303d47 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -1208,7 +1208,7 @@ temporary directory. A dictionary containing headers and values that the front-end proxy uses to indicate HTTPS requests. If the source IP is permitted by -``forwarded-allow-ips`` (below), *and* at least one request header matches +:ref:`forwarded-allow-ips` (below), *and* at least one request header matches a key-value pair listed in this dictionary, then Gunicorn will set ``wsgi.url_scheme`` to ``https``, so your application can tell that the request is secure. @@ -1235,16 +1235,21 @@ the headers defined here can not be passed directly from the client. **Default:** ``'127.0.0.1,::1'`` Front-end's IPs from which allowed to handle set secure headers. -(comma separate). +(comma separated). -Set to ``*`` to disable checking of Front-end IPs. This is useful for setups -where you don't know in advance the IP address of Front-end, but -instead have ensured via other means that none other than your -authorized Front-ends can access gunicorn. +Set to ``*`` to disable checking of front-end IPs. This is useful for setups +where you don't know in advance the IP address of front-end, but +instead have ensured via other means that only your +authorized front-ends can access Gunicorn. By default, the value of the ``FORWARDED_ALLOW_IPS`` environment variable. If it is not defined, the default is ``"127.0.0.1,::1"``. +.. note:: + + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + .. note:: The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of @@ -1370,13 +1375,19 @@ Example for stunnel config:: **Command line:** ``--proxy-allow-from`` -**Default:** ``'127.0.0.1'`` +**Default:** ``'127.0.0.1,::1'`` -Front-end's IPs from which allowed accept proxy requests (comma separate). +Front-end's IPs from which allowed accept proxy requests (comma separated). + +Set to ``*`` to disable checking of front-end IPs. This is useful for setups +where you don't know in advance the IP address of front-end, but +instead have ensured via other means that only your +authorized front-ends can access Gunicorn. + +.. note:: -Set to ``*`` to disable checking of Front-end IPs (useful for setups -where you don't know in advance the IP address of Front-end, but -you still trust the environment) + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. .. _raw-paste-global-conf: @@ -1486,14 +1497,15 @@ Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 **Command line:** ``--forwarder-headers`` -**Default:** ``'SCRIPT_NAME'`` +**Default:** ``'SCRIPT_NAME,PATH_INFO'`` A list containing upper-case header field names that the front-end proxy -sets, to be used in WSGI environment. +(see :ref:`forwarded-allow-ips`) sets, to be used in WSGI environment. -If headers named in this list are not present in the request, they will be ignored. +This option has no effect for headers not present in the request. -This option can be used to transfer SCRIPT_NAME and REMOTE_USER. +This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO`` +and ``REMOTE_USER``. It is important that your front-end proxy configuration ensures that the headers defined here can not be passed directly from the client. @@ -1518,8 +1530,8 @@ The value ``refuse`` will return an error if a request contains *any* such heade The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different header field names into the same environ name. -If the source IP is permitted by ``forwarded-allow-ips``, *and* the header name is -present in ``forwarder-headers``, the header is mapped into environment regardless of +If the source is permitted as explained in :ref:`forwarded-allow-ips`, *and* the header name is +present in :ref:`forwarder-headers`, the header is mapped into environment regardless of the state of this setting. Use with care and only if necessary and after considering if your problem could diff --git a/gunicorn/config.py b/gunicorn/config.py index f047074b8..6adaf1d4e 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -1250,7 +1250,7 @@ class SecureSchemeHeader(Setting): A dictionary containing headers and values that the front-end proxy uses to indicate HTTPS requests. If the source IP is permitted by - ``forwarded-allow-ips`` (below), *and* at least one request header matches + :ref:`forwarded-allow-ips` (below), *and* at least one request header matches a key-value pair listed in this dictionary, then Gunicorn will set ``wsgi.url_scheme`` to ``https``, so your application can tell that the request is secure. @@ -1278,16 +1278,21 @@ class ForwardedAllowIPS(Setting): default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1") desc = """\ Front-end's IPs from which allowed to handle set secure headers. - (comma separate). + (comma separated). - Set to ``*`` to disable checking of Front-end IPs. This is useful for setups - where you don't know in advance the IP address of Front-end, but - instead have ensured via other means that none other than your - authorized Front-ends can access gunicorn. + Set to ``*`` to disable checking of front-end IPs. This is useful for setups + where you don't know in advance the IP address of front-end, but + instead have ensured via other means that only your + authorized front-ends can access Gunicorn. By default, the value of the ``FORWARDED_ALLOW_IPS`` environment variable. If it is not defined, the default is ``"127.0.0.1,::1"``. + .. note:: + + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + .. note:: The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of @@ -2075,14 +2080,20 @@ class ProxyAllowFrom(Setting): name = "proxy_allow_ips" section = "Server Mechanics" cli = ["--proxy-allow-from"] - validator = validate_string_to_list - default = "127.0.0.1" + validator = validate_string_to_addr_list + default = "127.0.0.1,::1" desc = """\ - Front-end's IPs from which allowed accept proxy requests (comma separate). + Front-end's IPs from which allowed accept proxy requests (comma separated). + + Set to ``*`` to disable checking of front-end IPs. This is useful for setups + where you don't know in advance the IP address of front-end, but + instead have ensured via other means that only your + authorized front-ends can access Gunicorn. + + .. note:: - Set to ``*`` to disable checking of Front-end IPs (useful for setups - where you don't know in advance the IP address of Front-end, but - you still trust the environment) + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. """ @@ -2365,15 +2376,16 @@ class ForwarderHeaders(Setting): section = "Server Mechanics" cli = ["--forwarder-headers"] validator = validate_string_to_list - default = "SCRIPT_NAME" + default = "SCRIPT_NAME,PATH_INFO" desc = """\ A list containing upper-case header field names that the front-end proxy - sets, to be used in WSGI environment. + (see :ref:`forwarded-allow-ips`) sets, to be used in WSGI environment. - If headers named in this list are not present in the request, they will be ignored. + This option has no effect for headers not present in the request. - This option can be used to transfer SCRIPT_NAME and REMOTE_USER. + This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO`` + and ``REMOTE_USER``. It is important that your front-end proxy configuration ensures that the headers defined here can not be passed directly from the client. @@ -2398,8 +2410,8 @@ class HeaderMap(Setting): The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different header field names into the same environ name. - If the source IP is permitted by ``forwarded-allow-ips``, *and* the header name is - present in ``forwarder-headers``, the header is mapped into environment regardless of + If the source is permitted as explained in :ref:`forwarded-allow-ips`, *and* the header name is + present in :ref:`forwarder-headers`, the header is mapped into environment regardless of the state of this setting. Use with care and only if necessary and after considering if your problem could From 52538ca9070b5e7ead5d0fa731e82a622dc6f3ee Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 8 Aug 2024 18:32:23 +0200 Subject: [PATCH 5/7] docs: recommend SCRIPT_NAME=/subfolder --- docs/source/faq.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 10572c1d4..8c52a4865 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -13,8 +13,12 @@ How do I set SCRIPT_NAME? By default ``SCRIPT_NAME`` is an empty string. The value could be set by setting ``SCRIPT_NAME`` in the environment or as an HTTP header. Note that this headers contains and underscore, so it is only accepted from trusted -forwarders listed in the ``forwarded-allow-ips`` setting. +forwarders listed in the :ref:`forwarded-allow-ips` setting. +.. note:: + + If your application should appear in a subfolder, your ``SCRIPT_NAME`` + would typically start with single slash but contain no trailing slash. Server Stuff ============ From ffa48b581dcaa75f17fd2df263515e4266feeef6 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Thu, 8 Aug 2024 18:37:32 +0200 Subject: [PATCH 6/7] test: default change was intentional --- tests/test_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index 9afb4e49c..f41edc4d5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -166,6 +166,7 @@ def test_str_validation(): def test_str_to_addr_list_validation(): c = config.Config() + assert c.proxy_allow_ips == ["127.0.0.1", "::1"] assert c.forwarded_allow_ips == ["127.0.0.1", "::1"] c.set("forwarded_allow_ips", "127.0.0.1,192.0.2.1") assert c.forwarded_allow_ips == ["127.0.0.1", "192.0.2.1"] @@ -183,7 +184,7 @@ def test_str_to_addr_list_validation(): def test_str_to_list(): c = config.Config() - assert c.forwarder_headers == ["SCRIPT_NAME"] + assert c.forwarder_headers == ["SCRIPT_NAME", "PATH_INFO"] c.set("forwarder_headers", "SCRIPT_NAME,REMOTE_USER") assert c.forwarder_headers == ["SCRIPT_NAME", "REMOTE_USER"] c.set("forwarder_headers", "") From 256d474a7910bd605f2cc8c082b79c1ae55215a9 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Fri, 9 Aug 2024 00:28:08 +0200 Subject: [PATCH 7/7] docs: revert duped directive --- docs/source/deploy.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/deploy.rst b/docs/source/deploy.rst index c6bdbe114..5f8689793 100644 --- a/docs/source/deploy.rst +++ b/docs/source/deploy.rst @@ -255,7 +255,6 @@ to the newly created unix socket: # this user can be transiently created by systemd # DynamicUser=true RuntimeDirectory=gunicorn - WorkingDirectory=~ WorkingDirectory=/home/someuser/applicationroot ExecStart=/usr/bin/gunicorn applicationname.wsgi ExecReload=/bin/kill -s HUP $MAINPID