Zip Slip in Chi with Mutual Tls

Zip Slip in Chi with Mutual Tls — how this specific combination creates or exposes the vulnerability

Zip Slip is a path traversal vulnerability where user-controlled archive entries are used to traverse outside the intended extraction directory, often via malicious paths like ../../../etc/passwd. In Chi, a common Go HTTP router, developers often implement file upload or archive extraction handlers to serve or process uploaded content. When these handlers do not sanitize file paths and the service uses Mutual TLS (mTLS) for client authentication, a misconfiguration or trust boundary can allow an authenticated client to exploit path traversal during archive processing.

With mTLS, both client and server present certificates, and the server typically validates the client certificate before processing the request. This can create a false sense of security: authentication is enforced, but the handler’s file extraction logic remains unsafe. An authenticated client can still supply a malicious archive containing files with crafted paths. Because the server trusts the mTLS identity, it may skip relaxed validation that would otherwise be applied to anonymous requests. The combination of mTLS and unsafe extraction means the server reliably processes attacker-controlled archives, making Zip Slip more likely in environments that rely on mTLS for access control but omit strict path checks.

Chi’s idiomatic handlers often use middleware for authentication, where mTLS client verification is applied early. If the authorization check passes and the request proceeds to a route that extracts a zip file without cleaning paths, the vulnerability surfaces. Real-world examples include archive import features in admin panels or API endpoints that accept compressed backups. Even with strong client certificates, unchecked user input in archive contents can overwrite arbitrary files, potentially leading to remote code execution or sensitive file disclosure.

Mutual Tls-Specific Remediation in Chi — concrete code fixes

Secure handling requires validating archive contents regardless of mTLS status. Below are concrete Go examples using Chi, demonstrating safe extraction and mTLS configuration.

1. Configure Mutual TLS in Chi

Set up a TLS config that requests and verifies client certificates. This ensures only authorized clients can reach your endpoints, but extraction logic must still be hardened.

// server.go
package main

import (
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	serverMux := chi.NewRouter()
	serverMux.Use(middleware.Logger)

	// Load server certificate and key
	serverTLS := &tls.Config{
		Certificates: []tls.Certificate{loadServerCert()},
		ClientCAs:    loadClientCAPool(),
		ClientAuth:   tls.RequireAndVerifyClientCert, // enforce client cert validation
		MinVersion:   tls.VersionTLS12,
	}

	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: serverTLS,
		Handler:   serverMux,
	}

	serverMux.Get("/upload", uploadHandler)
	http.ListenAndServeTLS("", "", serverTLS)
}

func loadServerCert() tls.Certificate {
	cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		panic(err)
	}
	return cert
}

func loadClientCAPool() *x509.CertPool {
	caCert, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		panic(err)
	}
	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM(caCert)
	return pool
}

2. Safe Archive Extraction Handler

Always sanitize file paths using filepath.Clean and filepath.Rel to detect traversal attempts. Reject entries that escape the target directory.

// handlers.go
package main

import (
	"archive/zip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"

	"github.com/go-chi/chi/v5"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// r.TLS.ClientCertificate contains the verified client cert
	file, header, err := r.FormFile("archive")
	if err != nil {
		http.Error(w, "invalid file upload", http.StatusBadRequest)
		return
	}
	defer file.Close()

	tmpDir := "./uploads"
	rz, err := zip.NewReader(file, header.Size)
	if err != nil {
		http.Error(w, "invalid archive", http.StatusBadRequest)
		return
	}

	for _, f := range trz.File {
		// Clean and ensure path is within tmpDir
		cleanPath := filepath.Clean(f.Name)
		rel, err := filepath.Rel(tmpDir, filepath.Join(tmpDir, cleanPath))
		if err != nil || rel == ".." || filepath.IsAbs(rel) {
			http.Error(w, "invalid file path in archive", http.StatusBadRequest)
			return
		}

		dest := filepath.Join(tmpDir, cleanPath)
		if f.FileInfo().IsDir() {
			os.MkdirAll(dest, os.ModePerm)
			continue
		}

		src, err := f.Open()
		if err != nil {
			http.Error(w, "cannot open file in archive