Path traversal in authorization context in Traefik and HAProxy

Posted on Nov 23, 2021

In my previous post about Apache APISIX I have found path traversal in uri-blocker plugin. In this text I will focus on yet another ingress controller which is Traefik. It has feature called forward auth. At the end I will mention HAProxy ingress controller.

From docs:

The ForwardAuth middleware delegate the authentication to an external service. If the service response code is 2XX, access is granted and the original request is performed. Otherwise, the response from the authentication server is returned.

Setting the stage

First what we need to do is to install traefik in kubernetes:

helm repo add traefik https://helm.traefik.io/traefik
helm repo update
kubectl create namespace traefik
helm upgrade --install traefik \
    --namespace traefik \
    --set dashboard.enabled=true \
    --set rbac.enabled=true \
    --set="additionalArguments={--api.dashboard=true,--log.level=INFO,--providers.kubernetesingress.ingressclass=traefik-internal,--serversTransport.insecureSkipVerify=true}" \
    traefik/traefik --version 10.6.2

If you need more info about installation check here and here.

Check if it’s up and running: kubectl get pods -n traefik

Deployment yaml for ingress look like this:

kind: IngressRoute
apiVersion: traefik.containo.us/v1alpha1
metadata:
  name: services
  namespace: default
spec:
  entryPoints: 
    - web
  routes:
  - match: Host(`app.test`) && PathPrefix(`/public-service`)
    kind: Rule
    services:
    - name: public-service
      port: 8080
    middlewares:
      - name: public-stripprefix
      - name: auth-service
  - match: Host(`app.test`) && PathPrefix(`/protected-service`)
    kind: Rule
    services:
    - name: protected-service
      port: 8080
    middlewares:
      - name: protected-stripprefix
      - name: auth-service
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: protected-stripprefix
spec:
  stripPrefix:
    prefixes:
      - /protected-service
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: public-stripprefix
spec:
  stripPrefix:
    prefixes:
      - /public-service
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: auth-service
spec:
  forwardAuth:
    address: http://auth-service.default.svc.cluster.local:8080/verify

It’s using middleware that is specifying forwardAuth.

auth-service code is here:

from flask import Flask, Response, request
from http import HTTPStatus
import sys

app = Flask(__name__)

@app.route('/verify')
def verify():
    print(request.headers, file=sys.stderr)
    api_key = request.headers.get('X-Api-Key')
    forwarded_prefix = request.headers.get('X-Forwarded-Prefix')

    if forwarded_prefix and forwarded_prefix.startswith("/public-service"):
        return Response(status = HTTPStatus.NO_CONTENT)

    if api_key == "secret-api-key":  
        return Response(status = HTTPStatus.NO_CONTENT)

    return Response(status = HTTPStatus.UNAUTHORIZED)

Test

Traefik is using headers in communication with forwardAuth service. 

Headers

Those are:

  • X-Forwarded-For
  • X-Forwarded-Host
  • X-Forwarded-Method
  • X-Forwarded-Port
  • X-Forwarded-Prefix
  • X-Forwarded-Proto
  • X-Forwarded-Server
  • X-Forwarded-Uri
  • X-Real-Ip

Quite a bit, and maybe also some potential for bugs.

Exploitation

We are ready now to send some request and check how traefik is handling malicious payloads.

curl -v http://app.test/public-service/..%2Fprotected-service/protected

This is interesting. I have got 404. Which is completely different then Apache APISIX. It’s returned by Python not traefik. 

But which service got this request. Let’s check logs of public-service: kubectl logs public-service-7d56f8589d-59jqg

Oh! So traefik is not normalizing requests before executing them. That is important observation. 

And now logs from auth-service:

The X-Forwarded-Prefix is containing right value: /public-service and X-Forwarded-Uri is not having /public-service at all.

Good job traefik! 👍

I checked second payload with: curl --path-as-is -v http://app.test/public-service/../protected-service/protected but no luck.

HAProxy

I did similar research for HAProxy based haproxy-ingress as it’s also having option for external authentication. Results were very similar to those from Traefik. No bypass is possible, as HAProxy is not normalizing paths by default.

Summary

Trying with different ingress-controller was quite a fun 😃 However I was hoping for similar effect that I got with Apache APISIX. 

Traefik is not normalizing request paths before executing them. In my exploitation this is defense preventing from bypassing forwardAuth.

Versions of components that I was using:

minikube v1.23.2 on Microsoft Windows 10 Pro 10.0.19043 Build 19043; Kubernetesa v1.22.2 on Docker 20.10.8; traefik:2.5.3


Thanks for reading! You can follow me on Twitter.