Path traversal in authorization context in Emissary

Posted on Nov 24, 2021

After checking Apache APISIX and Traefik, for path traversal in authZ context, now I will research Emissary ingress.

In Emissary there is feature called Basic authentication, which is very similar to forward authentication discussed in Traefik.

Emissary-ingress can authenticate incoming requests before routing them to a backing service.

I can already tell you that Emissary is secure and you cannot bypass using path traversal. What is even better, it’s (and envoy) aware of this kind of security concern. There is full description in documentation. I encourage you to read it:

Emissary is setting normalize_path to true and path_with_escaped_slashes_action to KEEP_UNCHANGED. Which is preventing from path traversal bypass.  In this place I would like to point out difference between Emissary ingress and Edge Stack. The latter is using Filter as authentication service. I did not cover it in my research.

Setting the stage

Install Emissary ingress in Kubernetes:

helm repo add datawire https://app.getambassador.io
helm repo update
kubectl create namespace emissary && helm install emissary-ingress --devel --namespace emissary datawire/emissary-ingress && kubectl -n emissary wait --for condition=available --timeout=90s deploy -lapp.kubernetes.io/instance=emissary-ingress

If you need more info check here.

Check if all emissary pods are running: kubectl get pods -n emissary

Deploy listener:

apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
  name: emissary-ingress-listener-8080
  namespace: emissary
spec:
  port: 8080
  protocol: HTTP
  securityModel: XFP
  hostBinding:
    namespace:
      from: ALL

Deploy auth service definition and Emissary mappings:

apiVersion: getambassador.io/v3alpha1
kind: AuthService
metadata:
  name: authentication
spec:
  auth_service: "http://auth-service.default.svc.cluster.local:8080"
  proto: http
  path_prefix: "/verify"
  allowed_request_headers:
    - "X-Api-Key"
---
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: public-service
spec:
  hostname: "app.test"
  prefix: /public-service/
  service: public-service:8080
---
apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: protected-service
spec:
  hostname: "app.test"
  prefix: /protected-service/
  service: protected-service:8080

Code for auth-service is changed:

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

app = Flask(__name__)

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

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

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

    return Response(status = HTTPStatus.UNAUTHORIZED)

It’s not using headers as Traefik. Instead it’s passing requested uri as path into /verify route.

Test

Let’s check my payloads:

1° payload

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

I have got 404. And logs from public-service are giving answer why:

There is no route defined in public-service that is called /..%2Fprotected-service/protected 😃

What was logged by auth-service:

Everything is align. Prefix is public-service, %2F was not decoded (due to path_with_escaped_slashes_action envoy option) and auth-service got right path for decision.

2° payload

With second payload it’s a bit different as normalize_path envoy option is set to true.

curl --path-as-is -v http://app.test/public-service/../protected-service/protected

This time it’s not 404, but 401. Why ? Path was normalized. Everywhere. Not only in routes decision making but also in authentication service. Let’s check logs from auth-service:

Yep. Logs are confirming this.

Emissary vs ingress authZ bypass - 1 : 0 😃

Summary

Emissary is resistant for ingress authZ bypass using path traversal. I’m impressed that both Emissary and envoy are aware of this concern. And whole documentation about it is in place. Also default configuration is secure. I was even looking how to change it to less secure, but didn’t find a way 😅

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; Emissary 2.0.4


Thanks for reading! You can follow me on Twitter.