ingress-nginx icon indicating copy to clipboard operation
ingress-nginx copied to clipboard

Proxied Host and X-Forwarded-Host headers can contain arbitrary values when request-line contains full URI

Open nielsslot opened this issue 5 years ago • 19 comments

NGINX Ingress controller version: dev for 0.32 because of #5457, 0.34.1

Kubernetes version (use kubectl version): 1.18.2

Environment:

  • Cloud provider or hardware configuration: Bare metal
  • OS (e.g. from /etc/os-release): Ubuntu 18.04.4 LTS
  • Kernel (e.g. uname -a): 4.15.0-88-generic
  • Install tools: Kind
  • Others:

What happened:

Usually the Host and X-Forwarded-Host of the proxied request match the host field of the matching ingressRule of the ingress resource. Given an ingress resource for host 'foo.bar' pointing to an echoserver instance this looks like:

$ kubectl exec -it -n ingress-nginx $POD_NAME -- curl -H 'Host: foo.bar' localhost | grep host=
        host=foo.bar
        x-forwarded-host=foo.bar

But for requests that contain a full URI in the request-line of the HTTP request the proxied X-Forwarded-Host and Host headers can contain arbitrary values:

$ kubectl exec -it -n ingress-nginx $POD_NAME -- curl -H "Host: can-be-anything" -x http://127.0.0.1:80/ http://foo.bar/ | grep host=
        host=can-be-anything
        x-forwarded-host=can-be-anything

The hostname that was used to match the request is also not present in any other request header:

$ kubectl exec -it -n ingress-nginx $POD_NAME -- curl -H "Host: can-be-anything" -x http://127.0.0.1:80/ http://foo.bar/  | grep foo.bar
$ # returns nothing

The above curl command results in an HTTP requests that looks like:

GET http://foo.bar/ HTTP/1.1
Host: can-be-anything
User-Agent: curl/7.67.0
Accept: */*
Proxy-Connection: Keep-Alive

ingress-nginx seems to match the request to an ingress based on the hostname of the request-line URI, but doesn't forward this hostname to the backing service.

What you expected to happen:

I expected the Host and X-Forwarded-Host header in the proxied request to always equal the host field of the ingressRule of the ingress resource. I was expecting ingress-nginx to either:

  • match the request on the request-line URI with the ingress resource, proxy the request and forward the hostname it used to match the request in one of the headers (X-Forwarded-Host, Host or both)
  • match the request on the Host header and return a 404 status code since no such ingress exists

While a request with a full URI in the request-line is not common and not something any webbrowser or HTTP client would normally send, it is part of the HTTP RFC (see section 5.3.2 of RFC7230) and something NGINX supports. I can imagine that this behavior can lead to Host header injections and bad things for applications which rely on and trust the Host and X-Forwarded-Host headers coming from ingress-nginx.

How to reproduce it:

  • Install kind
  • Install the ingress controller
    • kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/baremetal/deploy.yaml
  • Install echoserver as a backend
    • kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/docs/examples/http-svc.yaml
  • Create an ingress
    • echo "
        apiVersion: networking.k8s.io/v1beta1
        kind: Ingress
        metadata:
          name: foo-bar
        spec:
          rules:
          - host: foo.bar
            http:
              paths:
              - backend:
                  serviceName: http-svc
                  servicePort: 80
                path: /
      " | kubectl apply -f -
      
  • Make a request with a URI in the request-line:
    • POD_NAME=$(kubectl get pods -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/component=controller -o NAME)
      kubectl exec -it -n ingress-nginx $POD_NAME -- curl -H "Host: can-be-anything" -x http://127.0.0.1:80/ http://foo.bar/
      

Anything else we need to know:

I'm submitting this as a bug because I expect the Host and X-Forwarded-Host headers to actually tell the backing service something about how the original request was matched to an ingress resource. But, I could see this issue as a feature request if the X-Forwarded-Host header is defined as the header containing the value of the original Host header. In that case I'd like to request a feature to add a header to ingress-nginx which does always contain the host of the ingress which matched the request. And perhaps additionally that the current behavior is clearly documented so that users know not to trust the Host and X-Forwarded-Host header values.

I found that adding the following annotation to the ingress resource causes the Host header in proxied requests to be as I expect it:

nginx.ingress.kubernetes.io/upstream-vhost: $host

I couldn't find a way to alter the X-Forwarded-Host header value.

/kind bug

nielsslot avatar Apr 29 '20 10:04 nielsslot

Issues go stale after 90d of inactivity. Mark the issue as fresh with /remove-lifecycle stale. Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta. /lifecycle stale

fejta-bot avatar Jul 28 '20 10:07 fejta-bot

I can still reproduce this issue with Ingress Nginx 0.34.1.

/remove-lifecycle stale

nielsslot avatar Aug 04 '20 10:08 nielsslot

Issues go stale after 90d of inactivity. Mark the issue as fresh with /remove-lifecycle stale. Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta. /lifecycle stale

fejta-bot avatar Nov 02 '20 10:11 fejta-bot

I can still reproduce this issue with Ingress Nginx 0.41.0.

As for the steps on how to reproduce. Here is a modified ingress YAML for the new networking.k8s.io/v1 API:

echo "
  apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    name: foo-bar
  spec:
    rules:
    - host: foo.bar
      http:
        paths:
        - backend:
            service:
              name: http-svc
              port:
                number: 80
          path: /
" | kubectl apply -f -

/remove-lifecycle stale

nielsslot avatar Nov 06 '20 10:11 nielsslot

Issues go stale after 90d of inactivity. Mark the issue as fresh with /remove-lifecycle stale. Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta. /lifecycle stale

fejta-bot avatar Feb 04 '21 11:02 fejta-bot

I can still reproduce this issue with Ingress Nginx 0.44.0.

Turns out the Ingress YAML from my previous comment didn't work as is. I had to add an explicit pathType:

echo "
  apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    name: foo-bar
  spec:
    rules:
    - host: foo.bar
      http:
        paths:
        - backend:
            service:
              name: http-svc
              port:
                number: 80
          path: /
          pathType: Prefix
" | kubectl apply -f -

/remove-lifecycle stale

nielsslot avatar Mar 03 '21 08:03 nielsslot

Issues go stale after 90d of inactivity. Mark the issue as fresh with /remove-lifecycle stale. Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-contributor-experience at kubernetes/community. /lifecycle stale

fejta-bot avatar Jun 01 '21 08:06 fejta-bot

/priority important-longterm

strongjz avatar Jun 22 '21 16:06 strongjz

Stale issues rot after 30d of inactivity. Mark the issue as fresh with /remove-lifecycle rotten. Rotten issues close after an additional 30d of inactivity.

If this issue is safe to close now please do so with /close.

Send feedback to sig-contributor-experience at kubernetes/community. /lifecycle rotten

fejta-bot avatar Jul 22 '21 16:07 fejta-bot

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Reopen this issue or PR with /reopen
  • Mark this issue or PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

k8s-triage-robot avatar Aug 21 '21 17:08 k8s-triage-robot

@k8s-triage-robot: Closing this issue.

In response to this:

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Reopen this issue or PR with /reopen
  • Mark this issue or PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

k8s-ci-robot avatar Aug 21 '21 17:08 k8s-ci-robot

I'm reopening this issue because it's still bugging me and I think I have an idea of how to fix it.

I believe that this bug could be related to the difference between the nginx variables $host and $http_host. There's a StackOverflow question about it. To summarize, the $http_host variable always contains the Host header of the HTTP request, the $host always contains the hostname as nginx used it to process the request.

If an HTTP request contains a full URL in the request-line then nginx uses the hostname from that URL as a value for $host and uses it to find the matching virtual host. The $http_host variable then simply contains the value of the Host header, which isn't used anywhere else to process the request.

From what I can tell, Ingress-nginx always uses to $http_host variable as a value for the outgoing X-Forwarded-Host header. If I look at the nginx.tmpl file, I see that the $best_http_host variable is set to $http_host and that this variable is used to set the proxied X-Forwarded-Host header. I haven't tested it, but I would guess that changing the set $best_http_host $http_host; line to set $best_http_host $host; would fix this issue. But I can't oversee any potential breakage from that change.

/reopen

nielsslot avatar Mar 07 '22 20:03 nielsslot

@nielsslot: Reopened this issue.

In response to this:

I'm reopening this issue because it's still bugging me and I think I have an idea of how to fix it.

I believe that this bug could be related to the difference between the nginx variables $host and $http_host. There's a StackOverflow question about it. To summarize, the $http_host variable always contains the Host header of the HTTP request, the $host always contains the hostname as nginx used it to process the request.

If an HTTP request contains a full URL in the request-line then nginx uses the hostname from that URL as a value for $host and uses it to find the matching virtual host. The $http_host variable then simply contains the value of the Host header, which isn't used anywhere else to process the request.

From what I can tell, Ingress-nginx always uses to $http_host variable as a value for the outgoing X-Forwarded-Host header. If I look at the nginx.tmpl file, I see that the $best_http_host variable is set to $http_host and that this variable is used to set the proxied X-Forwarded-Host header. I haven't tested it, but I would guess that changing the set $best_http_host $http_host; line to set $best_http_host $host; would fix this issue. But I can't oversee any potential breakage from that change.

/reopen

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

k8s-ci-robot avatar Mar 07 '22 20:03 k8s-ci-robot

/remove-lifecycle rotten

tallclair avatar Mar 08 '22 00:03 tallclair

/triage accepted /help

iamNoah1 avatar Apr 12 '22 08:04 iamNoah1

@iamNoah1: This request has been marked as needing help from a contributor.

Guidelines

Please ensure that the issue body includes answers to the following questions:

  • Why are we solving this issue?
  • To address this issue, are there any code changes? If there are code changes, what needs to be done in the code and what places can the assignee treat as reference points?
  • Does this issue have zero to low barrier of entry?
  • How can the assignee reach out to you for help?

For more details on the requirements of such an issue, please see here and ensure that they are met.

If this request no longer meets these requirements, the label can be removed by commenting with the /remove-help command.

In response to this:

/triage accepted /help

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

k8s-ci-robot avatar Apr 12 '22 08:04 k8s-ci-robot

The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle stale
  • Mark this issue or PR as rotten with /lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

k8s-triage-robot avatar Jul 11 '22 09:07 k8s-triage-robot

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle rotten

k8s-triage-robot avatar Aug 10 '22 10:08 k8s-triage-robot

The issue has been marked as an important bug and triaged. Such issues are automatically marked as frozen when hitting the rotten state to avoid missing important bugs.

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle frozen

k8s-triage-robot avatar Aug 10 '22 12:08 k8s-triage-robot

This issue has not been updated in over 1 year, and should be re-triaged.

You can:

  • Confirm that this issue is still relevant with /triage accepted (org members only)
  • Close this issue with /close

For more details on the triage process, see https://www.kubernetes.dev/docs/guide/issue-triage/

/remove-triage accepted

k8s-triage-robot avatar Jan 18 '24 22:01 k8s-triage-robot