CVE-2023-43810 (Python, published 2023-10-06T14:15Z)
Description: value: OpenTelemetry, also known as OTel for short, is a vendor-neutral open-source Observability framework for instrumenting, generating, collecting, and exporting telemetry data such as traces, metrics, logs. Autoinstrumentation out of the box adds the label `http_method` that has unbound cardinality. It leads to the server's potential memory exhaustion when many malicious requests are sent. HTTP method for requests can be easily set by an attacker to be random and long. In order to be affected program has to be instrumented for HTTP handlers and does not filter any unknown HTTP methods on the level of CDN, LB, previous middleware, etc. This issue has been patched in version 0.41b0.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-400 \u2014 Uncontrolled Resource Consumption
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
Repository: https://github.com/open-telemetry/opentelemetry-python-contrib
Files changed:
instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py
instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py
instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py
util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py
util/opentelemetry-util-http/tests/test_sanitize_method.py
Commit message: Merge pull request from GHSA-5rv5-6h4r-h22v

* Fix unbound cardinality for label http_method in wsgi based middlewares

* cr: rename file

* cr: change label UNKNOWN to NONSTANDARD

* Update instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py

---------

Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>

Vulnerable code:
        metrics_list = self.memory_metrics_reader.get_metrics_data()
        for resource_metric in metrics_list.resource_metrics:
            for scope_metrics in resource_metric.scope_metrics:
                for metric in scope_metrics.metrics:
                    for point in list(metric.data.data_points):
                        if isinstance(point, HistogramDataPoint):
                            self.assertDictEqual(
                                expected_duration_attributes,
                                dict(point.attributes),
                            )
                            self.assertEqual(point.count, 1)
                        elif isinstance(point, NumberDataPoint):
                            self.assertDictEqual(
                                expected_requests_count_attributes,
                                dict(point.attributes),
                            )
                            self.assertEqual(point.value, 0)
        SpanAttributes.HTTP_METHOD: environ.get("REQUEST_METHOD"),
    method = environ.get("REQUEST_METHOD", "").strip()

Fixed code:
    OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
    OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS,
    def _assert_basic_metric(self, expected_duration_attributes, expected_requests_count_attributes):
        metrics_list = self.memory_metrics_reader.get_metrics_data()
        for resource_metric in metrics_list.resource_metrics:
            for scope_metrics in resource_metric.scope_metrics:
                for metric in scope_metrics.metrics:
                    for point in list(metric.data.data_points):
                        if isinstance(point, HistogramDataPoint):
                            self.assertDictEqual(
                                expected_duration_attributes,
                                dict(point.attributes),
                            )
                            self.assertEqual(point.count, 1)
                        elif isinstance(point, NumberDataPoint):
                            self.assertDictEqual(
                                expected_requests_count_attributes,
                                dict(point.attributes),
                            )
                            self.assertEqual(point.value, 0)

        self._assert_basic_metric(
            expected_duration_attributes,
            expected_requests_count_attributes,
        )

    def test_basic_metric_nonstandard_http_method_success(self):
        self.client.open("/hello/756", method="NONSTANDARD")
        expected_duration_attributes = {
            "http.method": "UNKNOWN",
            "http.host": "localhost",
            "http.scheme": "http",
            "http.flavor": "1.1",
            "http.server_name": "localhost",
            "net.host.port": 80,
            "http.status_code": 405,
        }
        expected_requests_count_attributes = {
            "http.method": "UNKNOWN",
            "http.host": "localhost",
            "http.scheme": "http",
            "http.flavor": "1.1",
            "http.server_name": "localhost",
        }
        self._assert_basic_metric(
            expected_duration_attributes,
            expected_requests_count_attributes,
        )

    @patch.dict(
    "os.environ",
        {
            OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1",
        },
    )
    def test_basic_metric_nonstandard_http_method_allowed_success(self):
        self.client.open("/hello/756", method="NONSTANDARD")
        expected_duration_attributes = {
            "http.method": "NONSTANDARD",
            "http.host": "localhost",
            "http.scheme": "http",
            "http.flavor": "1.1",
            "http.server_name": "localhost",
            "net.host.port": 80,
            "http.status_code": 405,
        }
        expected_requests_count_attributes = {
            "http.method": "NONSTANDARD",
            "http.host": "localhost",
            "http.scheme": "http",
            "http.flavor": "1.1",
            "http.server_name": "localhost",
        }
        self._assert_basic_metric(
            expected_duration_attributes,
            expected_requests_count_attributes,
        )
Sanitizing methods
******************
In order to prevent unbound cardinality for HTTP methods by default nonstandard ones are labeled as ``NONSTANDARD``.
To record all of the names set the environment variable  ``OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS``
to a value that evaluates to true, e.g. ``1``.

    sanitize_method,
        SpanAttributes.HTTP_METHOD: sanitize_method(environ.get("REQUEST_METHOD")),
    method = sanitize_method(environ.get("REQUEST_METHOD", "").strip())
    OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS,
    def test_nonstandard_http_method(self):
        self.environ["REQUEST_METHOD"]= "NONSTANDARD"
        app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
        response = app(self.environ, self.start_response)
        self.validate_response(response, span_name="HTTP UNKNOWN", http_method="UNKNOWN")

    @mock.patch.dict(
    "os.environ",
        {
            OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1",
        },
    )
    def test_nonstandard_http_method_allowed(self):
        self.environ["REQUEST_METHOD"]= "NONSTANDARD"
        app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
        response = app(self.environ, self.start_response)
        self.validate_response(response, span_name="HTTP NONSTANDARD", http_method="NONSTANDARD")

OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS = (
    "OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS"
)

def sanitize_method(method: str | None) -> str | None:
    if method is None:
        return None
    method = method.upper()
    if (environ.get(OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS) or
        # Based on https://www.rfc-editor.org/rfc/rfc7231#section-4.1 and https://www.rfc-editor.org/rfc/rfc5789#section-2.
        method in ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]):
        return method
    return "NONSTANDARD"
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
from unittest.mock import patch

from opentelemetry.util.http import (
    OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS,
    sanitize_method,
)

class TestSanitizeMethod(unittest.TestCase):
    def test_standard_method_uppercase(self):
        method = sanitize_method("GET")
        self.assertEqual(method, "GET")

    def test_standard_method_lowercase(self):
        method = sanitize_method("get")
        self.assertEqual(method, "GET")

    def test_nonstandard_method(self):
        method = sanitize_method("UNKNOWN")
        self.assertEqual(method, "NONSTANDARD")

    @patch.dict(
    "os.environ",
        {
            OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1",
        },
    )
    def test_nonstandard_method_allowed(self):
        method = sanitize_method("UNKNOWN")
        self.assertEqual(method, "NONSTANDARD")