Path traversal in authorization context in Emissary
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:
- rejecting-client-requests-with-escaped-slashes - although it’s not directly for Emissary. It’s describing well envoy concerts for escaped slashes
- GHSA-xcx5-93pw-jw2w (CVE-2019–9901) - description of risks associated with normalizing paths in envoy
- envoy http connection manager options - look for two particular:
Emissary is setting
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
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
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
Let’s check my payloads:
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
What was logged by
Everything is align. Prefix is
%2F was not decoded (due to
path_with_escaped_slashes_action envoy option) and
auth-service got right path for decision.
With second payload it’s a bit different as
normalize_path envoy option is set to
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
Yep. Logs are confirming this.
Emissary vs ingress authZ bypass - 1 : 0 😃
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
Other articles from this series
- CVE-2021-43557: Apache APISIX: Path traversal in request_uri variable
- Path traversal in authorization context in Traefik and HAProxy
- Path traversal in authorization context in Kong and F5 NGINX
- Bug bounty tips for nginx $request_uri path traversal bypass
- Hunting for buggy authentication/authorization services on github
Thanks for reading! You can follow me on Twitter.