From f2156ae405c029265e42200d024fd5d1b0cc1ee5 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Sat, 30 Mar 2024 05:35:18 +0100 Subject: [PATCH 1/2] Reject invalid HTTP methods and resources This change addresses the issue that currently, any HTTP method is handled by returning success and metrics data, which causes network scanners to report issues. Details: * This change rejects any HTTP methods and resources other than the following: OPTIONS (any) - returns 200 and an 'Allow' header indicating allowed methods GET (any) - returns 200 and metrics GET /favicon.ico - returns 200 and no body (this is no change) Other HTTP methods than these are rejected with 405 "Method Not Allowed" and an 'Allow' header indicating the allowed HTTP methods. Any returned HTTP errors are also displayed in the response body after a hash sign and with a brief hint, e.g. "# HTTP 405 Method Not Allowed: XXX; use OPTIONS or GET". * Needed to pin asgiref to ==3.6.0 also for py3.8 to circumvent the same error as for pypy3.8. Signed-off-by: Andreas Maier --- docs/content/exporting/http/_index.md | 21 ++++++++++++++++++++- prometheus_client/exposition.py | 13 ++++++++++++- tox.ini | 4 ++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/content/exporting/http/_index.md b/docs/content/exporting/http/_index.md index 71edc7e3..dc1b8f2c 100644 --- a/docs/content/exporting/http/_index.md +++ b/docs/content/exporting/http/_index.md @@ -52,4 +52,23 @@ chain is used (see Python [ssl.SSLContext.load_default_certs()](https://docs.pyt from prometheus_client import start_http_server start_http_server(8000, certfile="server.crt", keyfile="server.key") -``` \ No newline at end of file +``` + +# Supported HTTP methods + +The prometheus client will handle the following HTTP methods and resources: + +* `OPTIONS (any)` - returns HTTP status 200 and an 'Allow' header indicating the + allowed methods (OPTIONS, GET) +* `GET (any)` - returns HTTP status 200 and the metrics data +* `GET /favicon.ico` - returns HTTP status 200 and an empty response body. Some + browsers support this to display the returned icon in the browser tab. + +Other HTTP methods than these are rejected with HTTP status 405 "Method Not Allowed" +and an 'Allow' header indicating the allowed methods (OPTIONS, GET). + +Any returned HTTP errors are also displayed in the response body after a hash +sign and with a brief hint. Example: +``` +# HTTP 405 Method Not Allowed: XXX; use OPTIONS or GET +``` diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 3a47917c..87dae7aa 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -118,12 +118,23 @@ def prometheus_app(environ, start_response): accept_header = environ.get('HTTP_ACCEPT') accept_encoding_header = environ.get('HTTP_ACCEPT_ENCODING') params = parse_qs(environ.get('QUERY_STRING', '')) - if environ['PATH_INFO'] == '/favicon.ico': + + if environ['REQUEST_METHOD'] == 'OPTIONS': + status = '200 OK' + headers = [('Allow', 'OPTIONS,GET')] + output = b'' + elif environ['REQUEST_METHOD'] != 'GET': + status = '405 Method Not Allowed' + headers = [('Allow', 'OPTIONS,GET')] + output = '# HTTP {}: {}; use OPTIONS or GET\n'.format(status, environ['REQUEST_METHOD']).encode() + elif environ['PATH_INFO'] == '/favicon.ico': # Serve empty response for browsers status = '200 OK' headers = [('', '')] output = b'' else: + # Note: For backwards compatibility, the URI path for GET is not + # constrained to the documented /metrics, but any path is allowed. # Bake output status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression) # Return output diff --git a/tox.ini b/tox.ini index 6d6b756c..b9e5e360 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ deps = pytest attrs {py3.8,pypy3.8}: twisted - py3.8: asgiref - # See https://github.com/django/asgiref/issues/393 for why we need to pin asgiref for pypy + # See https://github.com/django/asgiref/issues/393 for why we need to pin asgiref + py3.8: asgiref==3.6.0 pypy3.8: asgiref==3.6.0 commands = coverage run --parallel -m pytest {posargs} From affd81f51aff9a9cf09b331815f006fc86947b32 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Mon, 29 Jul 2024 15:38:36 +0200 Subject: [PATCH 2/2] Removed CBC ciphers to address CVE-2013-0169 (LUCKY13) Details: * This change removes the following CBC ciphers from the default set of ciphers in order to address CVE-2013-0169 (LUCKY13): - ECDHE-ECDSA-AES256-SHA384 - ECDHE-RSA-AES256-SHA384 - ECDHE-ECDSA-AES128-SHA256 - ECDHE-RSA-AES128-SHA256 This is done by listing them in the code, i.e. without any way to configure that by the user. Signed-off-by: Andreas Maier --- prometheus_client/exposition.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 87dae7aa..43360c76 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -181,6 +181,22 @@ def _get_ssl_ctx( """Load context supports SSL.""" ssl_cxt = ssl.SSLContext(protocol=protocol) + # The following chipers will be removed if the default cipher set contains + # them. The reason for each cipher is stated in the comment. + remove_cipher_names = [ + "ECDHE-ECDSA-AES256-SHA384", # is a CBC cipher (CVE-2013-0169) + "ECDHE-RSA-AES256-SHA384", # is a CBC cipher (CVE-2013-0169) + "ECDHE-ECDSA-AES128-SHA256", # is a CBC cipher (CVE-2013-0169) + "ECDHE-RSA-AES128-SHA256", # is a CBC cipher (CVE-2013-0169) + ] + cipher_names = [c['name'] for c in ssl_cxt.get_ciphers()] + for cipher_name in remove_cipher_names: + try: + cipher_names.remove(cipher_name) + except ValueError: + pass + ssl_cxt.set_ciphers(':'.join(cipher_names)) + if cafile is not None or capath is not None: try: ssl_cxt.load_verify_locations(cafile, capath)